赞
踩
欢迎关注博主公众号「Java大师」,
专注于分享Java领域干货文章http://www.javaman.cn/sb2/jwt
1)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
认证管理
流程图解读:
1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。
3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。
授权管理
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。
package com.ds.book.entity; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import java.util.Collection; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; /** * <p> * * </p> * * @author java大师 * @since 2023-03-17 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("t_user") public class User implements Serializable, UserDetails { private static final long serialVersionUID = 1L; private Integer id; /** * 登录名 */ private String name; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 是否有效:1-有效;0-无效 */ private String status; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles .stream() .map(role -> new SimpleGrantedAuthority(role.getRoleCode())) .collect(Collectors.toList()); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
登录成功后,将UserDetails的roles设置到用户中
package com.ds.book.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ds.book.entity.User; import com.ds.book.mapper.UserMapper; import com.ds.book.service.IUserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; /** * <p> * 服务实现类 * </p> * * @author java大师 * @since 2023-03-17 */ @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService, UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User loginUser = userMapper.selectOne(new QueryWrapper<User>().eq("username", username)); if (loginUser == null){ throw new UsernameNotFoundException("用户名或密码错误"); } loginUser.setRoles(userMapper.getRolesByUserId(loginUser.getId())); return loginUser; } }
将我们自己的UserDetailService注入springsecurity
package com.ds.book.config; import com.ds.book.filter.JwtTokenFilter; import com.ds.book.service.impl.UserServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserServiceImpl userService; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //注入我们自己的UserDetailService @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } }
问题:前后端分离项目,通常不会使用springsecurity自带的登录界面,登录界面由前端完成,后台只需要提供响应的服务即可,且目前主流不会采用session去存取用户,后端会返回响应的token,前端访问的时候,会在headers里面带入token.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pW7SxQqz-1679881634010)(D:\个人\公众号\网站\dsblog开发手册\image-20230322114008186.png)]
Jwt token由Header、Payload、Signature三部分组成,这三部分之间以小数点”.”连接,JWT token长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU
token解析后长这样:
header部分,有令牌的类型(JWT)和签名算法名称(HS256):
{
“alg”: “HS256”,
“typ”: “JWT”
}
Payload部分,有效负载,这部分可以放任何你想放的数据:
{
“sub”: “1234567890”,
“name”: “John Doe”,
“iat”: 1516239022
}
Signature签名部分,由于这部分是使用header和payload部分计算的,所以还可以以此来验证payload部分有没有被篡改:
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
123456 //这里是密钥,只要够复杂,一般不会被破解
)
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
package com.ds.book.tool; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID; /** * JWT工具类 */ public class JwtUtil { //有效期为 public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时 //设置秘钥明文 public static final String JWT_KEY = "dashii"; public static String getUUID(){ String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间 return builder.compact(); } /** * 生成jtw * @param subject token中要存放的数据(json格式) * @param ttlMillis token超时时间 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if(ttlMillis==null){ ttlMillis= JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) //唯一的ID .setSubject(subject) // 主题 可以是JSON数据 .setIssuer("dashi") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥 .setExpiration(expDate); } /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间 return builder.compact(); } /** * 生成加密后的秘钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 解析 * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } }
package com.ds.book.filter; import com.ds.book.entity.User; import com.ds.book.mapper.UserMapper; import com.ds.book.service.IMenuService; import com.ds.book.service.IUserService; import com.ds.book.tool.JwtUtil; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtTokenFilter extends OncePerRequestFilter { @Autowired private IUserService userService; @Autowired private UserMapper userMapper; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { //1、获取token String token = httpServletRequest.getHeader("token"); if (StringUtils.isEmpty(token)){ filterChain.doFilter(httpServletRequest,httpServletResponse); return; } String userId; try { Claims claims = JwtUtil.parseJWT(token); userId = claims.getSubject(); } catch (Exception exception) { exception.printStackTrace(); throw new RuntimeException("token非法"); } User user = userService.getUserById(Integer.parseInt(userId)); user.setRoles(userMapper.getRolesByUserId(Integer.parseInt(userId))); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(httpServletRequest,httpServletResponse); } }
在springsecurity中,第一个经过的过滤器是UsernamePasswordAuthenticationFilter,所以前后端分离的项目,我们自己定义的过滤器要放在这个过滤器前面,具体配置如下
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.cors();
}
1)主启动类上添加EnableGlobalMethodSecurity注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
@SpringBootApplication
@MapperScan("com.ds.book.mapper")
public class BookSysApplication {
public static void main(String[] args) {
SpringApplication.run(BookSysApplication.class,args);
}
}
2)Controller方法上添加@PreAuthorize注解
@RestController
public class HelloController {
@GetMapping("/hello")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String hello(){
return "hello";
}
}
1)创建我们自己的FilterInvocationSecurityMetadataSource,实现getAttributes方法,获取请求url所需要的角色
@Component public class MySecurtiMetaDataSource implements FilterInvocationSecurityMetadataSource { @Autowired private IMenuService menuService; AntPathMatcher antPathMatcher = new AntPathMatcher(); //获取访问url需要的角色,例如:/sys/user需要ROLE_ADMIN角色,访问sys/user时获取到必须要有ROLE_ADMIN角色。返回 Collection<ConfigAttribute> @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestURI = ((FilterInvocation) object).getRequest().getRequestURI(); //获取所有的菜单及角色 List<Menu> menus = menuService.getMenus(); for (Menu menu : menus) { if (antPathMatcher.match(menu.getUrl(),requestURI)){ String[] roles = menu.getRoles().stream().map(role -> role.getRoleCode()).toArray(String[]::new); return SecurityConfig.createList(roles); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return false; } }
2)创建我们自己的决策管理器AccessDecisionManager,实现decide方法,判断步骤1)中获取到的角色和我们目前登录的角色是否相同,相同则允许访问,不相同则不允许访问,
@Component public class MyAccessDecisionManager implements AccessDecisionManager { //1、认证通过后,会往authentication中填充用户信息 //2、拿authentication中的权限与上一步获取到的角色信息进行比对,比对成功后,允许访问 @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (ConfigAttribute configAttribute : configAttributes) { for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(configAttribute.getAttribute())){ return; } } } throw new AccessDeniedException("权限不足,请联系管理员"); } @Override public boolean supports(ConfigAttribute attribute) { return false; } @Override public boolean supports(Class<?> clazz) { return false; } }
3)在SecurityConfig中,添加后置处理器(增强器),让springsecurity使用我们自己的datametasource和decisionMananger
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MySecurtiMetaDataSource mySecurtiMetaDataSource; @Autowired private MyAccessDecisionManager myAccessDecisionManager; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired private UserServiceImpl userService; @Autowired private JwtTokenFilter jwtTokenFilter; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() //后置处理器,使用我们自己的FilterSecurityInterceptor拦截器配置 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor> () { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(mySecurtiMetaDataSource); o.setAccessDecisionManager(myAccessDecisionManager); return o; } }) .and() .headers().cacheControl(); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); http.cors(); } }
1)前端渲染工具类
public class WebUtils { /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
2)未登录异常处理,实现commence方法
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Result result = new Result(401,"未登录,请先登录",null);
String json = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,json);
}
}
3)授权失败异常处理,实现Handle方法
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
Result result = new Result(403,"权限不足请联系管理员",null);
String s = JSON.toJSONString(result);
WebUtils.renderString(httpServletResponse,s);
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。