当前位置:   article > 正文

springboot-安全认证security+jwt总结_resultutils.writejavascript

resultutils.writejavascript

目录

一、背景

二、基本jar依赖引入

三、security模块

1、编写配置类

2、UnauthorizedHandler代码

3、security验证用户名和密码的部分

四、jwt模块

1、jwt原理部分

2、jwt一共需要四个类

五、总结


一、背景

要做一个后台管理系统,会引入多个系统,这就需要做用户认证和权限管理。用户认证通过token来实现,市面上的技术有很多,我这里仅仅来说明一下security+jwt的一种实现过程,没有做页面,需要做页面的同学自行实现。

有些容易入坑的点,我看别的资料没有说太清楚,这里记录下,希望能帮助到跳坑的同学。

二、基本jar依赖引入

  1. <!-- security -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>
  6. <!-- jwt依赖 -->
  7. <dependency>
  8. <groupId>io.jsonwebtoken</groupId>
  9. <artifactId>jjwt</artifactId>
  10. <version>0.9.1</version>
  11. </dependency>

总结:

我看有的帖子也引入了jjwt的API、impl包,我这里没有用到,实现权限控制和token校验两个完全够用

三、security模块

1、编写配置类

  1. @Configuration
  2. @EnableWebSecurity
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  5. //操作用户
  6. @Autowired
  7. FfUserService ffUserService;
  8. //token校验
  9. @Autowired
  10. JwtAuthenPreFilter jwtAuthenPreFilter;
  11. /**
  12. *token异常
  13. */
  14. @Autowired
  15. UnauthorizedHandler unauthorizedHandler;
  16. //配置放行策略
  17. @Value("${jwt.security.antMatchers}")
  18. private String antMatchers;
  19. /**
  20. * 密码加密算法
  21. *
  22. * @return
  23. */
  24. @Bean
  25. public PasswordEncoder passwordEncoder() {
  26. return new BCryptPasswordEncoder(10);
  27. }
  28. /**
  29. * 验证用户来源,主要是验证账号和密码
  30. *
  31. * @param auth
  32. * @throws Exception
  33. */
  34. @Override
  35. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  36. auth.userDetailsService(ffUserService).passwordEncoder(passwordEncoder());
  37. }
  38. /**
  39. * 忽略策略
  40. * @param web
  41. * @throws Exception
  42. */
  43. @Override
  44. public void configure(WebSecurity web) throws Exception {
  45. web.ignoring().antMatchers(antMatchers.split(","));
  46. }
  47. /**
  48. * 用户授权
  49. * @param http
  50. * @throws Exception
  51. */
  52. @Override
  53. protected void configure(HttpSecurity http) throws Exception {
  54. http
  55. .authorizeRequests()
  56. .anyRequest().authenticated() // 所有的验证都需要验证
  57. .and()
  58. .csrf().disable() // 禁用 Spring Security 自带的跨域处理
  59. // 定制我们自己的 session 策略:调整为让 Spring Security 不创建和使用 session
  60. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  61. .and()
  62. .exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
  63. // 将自定义的过滤器添加在指定过滤器之前
  64. http.addFilterBefore(jwtAuthenPreFilter, FilterSecurityInterceptor.class);
  65. // 禁用缓存
  66. http.headers().cacheControl();
  67. }
  68. }

总结:

  • 需要放行的可以在两个地方配置,第一种如上图;第二种可以在第二个configure中配置。比如:.antMatchers(antMatchers.split(",")).permitAll()
  • 在第二个configure中这里特别注意一下配置的顺序,exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint())如果放在前面会导致放行策略不生效。
  • security是通过用户名和密码来实现认证的,不一定能满足实际业务需要,所以要扩展,前后端分离目前常用的做法就是基于usertoken的,即上面的自定义的jwtAuthenPreFilter过滤器
  • addFilterAfter: 将自定义的过滤器添加在指定过滤器之后
  • addFilterBefore:将自定义的过滤器添加在指定过滤器之前
  • addFilter:添加一个过滤器,但必须是Spring Security自身提供的过滤器实例或其子过滤器
  • addFilterAt: 添加一个过滤器在指定过滤器位置

2、UnauthorizedHandler代码

  1. @Component
  2. @Slf4j
  3. public class UnauthorizedHandler implements AuthenticationEntryPoint {
  4. @Override
  5. public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
  6. //用户登录时身份认证未通过
  7. if(e instanceof BadCredentialsException){
  8. //用户登录时身份认证失败
  9. ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.TOKEN_INVALID.getCode(), e.getMessage());
  10. }else if (e instanceof InsufficientAuthenticationException){
  11. //缺少请求头参数,Authorization传递是token值,所以是参数是必须的
  12. ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.NO_TOKEN.getCode(), ErrorCodeEnum.NO_TOKEN.getMessage());
  13. }else{
  14. //用户token无效
  15. ResultUtil.writeJavaScript(httpServletResponse, ErrorCodeEnum.TOKEN_INVALID.getCode(), ErrorCodeEnum.TOKEN_INVALID.getMessage());
  16. }
  17. }
  18. }

注意:

  • 接口中的逻辑异常要捕获,不然会被拦截报token异常就不美观了,也可以完善这个类。
  • 也可以扩展单独的异常处理模块做统一处理,但是业务异常我还是推荐根据业务场景来单独处理,一味的追求统一处理不见得都是好事。

3、security验证用户名和密码的部分

  • 网上资料很多,大家自己补充

四、jwt模块

1、jwt原理部分

  • 网上资料很多,大家自己补充

2、jwt一共需要四个类

  • JwtAuthenPreFilter:token校验和有关业务,这部分可以根据自己项目来实现
  1. @Component
  2. @Slf4j
  3. public class JwtAuthenPreFilter extends OncePerRequestFilter {
  4. @Autowired
  5. private JwtTokenUtil jwtTokenUtil;
  6. //@Autowired
  7. //private RedisUtil redisUtil;
  8. /**
  9. * 防止filter被执行两次
  10. */
  11. private static final String FILTER_APPLIED = "__spring_security_JwtAuthenPreFilter_filterApplied";
  12. @Value("${jwt.header:Authorization}")
  13. private String tokenHeader;
  14. @Value("${jwt.tokenHead:Bearer}")
  15. private String tokenHead;
  16. /**
  17. * 距离快过期多久刷新令牌
  18. */
  19. @Value("${jwt.token.subRefresh:#{10*60}}")
  20. private Long subRefresh;
  21. // 不需要认证的接口
  22. @Value("${jwt.security.antMatchers}")
  23. private String antMatchers;
  24. @Autowired
  25. private FfUserService ffUserService ;
  26. public JwtAuthenPreFilter() {
  27. }
  28. @Override
  29. protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
  30. if (httpServletRequest.getAttribute(FILTER_APPLIED) != null) {
  31. filterChain.doFilter(httpServletRequest, httpServletResponse);
  32. return;
  33. }
  34. httpServletRequest.setAttribute(FILTER_APPLIED, true);
  35. //过滤掉不需要token验证的url
  36. SkipPathAntMatcher skipPathRequestMatcher = new SkipPathAntMatcher(Arrays.asList(antMatchers.split(",")));
  37. if (skipPathRequestMatcher.matches(httpServletRequest)) {
  38. filterChain.doFilter(httpServletRequest, httpServletResponse);
  39. } else {
  40. try {
  41. //1.判断是否有效 2.判断是否过期 3.如果未过期的,且过期时间小于10分钟的延长过期时间,并在当前response返回新的header,客户端需替换此令牌
  42. String authHeader = httpServletRequest.getHeader(this.tokenHeader);
  43. if (authHeader != null && authHeader.startsWith(tokenHead)) {
  44. final String authToken = authHeader.substring(tokenHead.length());
  45. JWTUserDetail userDetail = jwtTokenUtil.getUserFromToken(authToken);
  46. if (ObjectUtils.isEmpty(userDetail)) {
  47. log.info("令牌非法,解析失败{}!", authToken);
  48. throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());
  49. }
  50. if (jwtTokenUtil.isTokenExpired(authToken)) {
  51. log.info("令牌已失效!{}", authToken);
  52. throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());
  53. }
  54. //令牌快过期生成新的令牌并设置到返回头中,客户端在每次的restful请求如果发现有就替换原值
  55. if (new Date(System.currentTimeMillis() - subRefresh).after(jwtTokenUtil.getExpirationDateFromToken(authToken))) {
  56. String resAuthToken = jwtTokenUtil.generateToken(userDetail);
  57. httpServletResponse.setHeader(tokenHeader, tokenHead + resAuthToken);
  58. }
  59. JwtTokenUtil.LOCAL_USER.set(userDetail);
  60. UserDetails userDetails = ffUserService.loadUserByUsername(userDetail.getLoginName());
  61. UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
  62. authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
  63. SecurityContextHolder.getContext().setAuthentication(authentication);
  64. } else {
  65. //需要校验却无用户token
  66. log.info("无header请求-->" + httpServletRequest.getRequestURI());
  67. throw new InsufficientAuthenticationException(ErrorCodeEnum.NO_TOKEN.getMessage());
  68. }
  69. } catch (Exception e) {
  70. //log.info("令牌解析失败!", e);
  71. throw new BadCredentialsException(ErrorCodeEnum.TOKEN_INVALID.getMessage());
  72. }
  73. filterChain.doFilter(httpServletRequest, httpServletResponse);
  74. //调用完成后清除
  75. JwtTokenUtil.LOCAL_USER.remove();
  76. }
  77. }
  78. }
  • JWTUserDetail:token转换成user类
  1. @Data
  2. public class JWTUserDetail implements Serializable {
  3. /**
  4. * 登陆用户编号
  5. */
  6. private long userId;
  7. /**
  8. * 登陆用户账户名称(可能为手机号邮箱或者名称用户维度唯一)
  9. */
  10. private String loginName;
  11. /**
  12. * 登陆用户类型
  13. */
  14. private UserType userType;
  15. /**
  16. * 登陆用户凭证
  17. */
  18. private String jwtToken;
  19. /**
  20. * 登陆时间
  21. */
  22. private Date loginTime;
  23. private static ObjectMapper mapper = new ObjectMapper();
  24. public enum UserType {
  25. User("USER", 1),
  26. Operator("OPT", 2),
  27. Erp("ERP", 3);
  28. private String name;
  29. private int index;
  30. private UserType(String name, int index) {
  31. this.name = name;
  32. this.index = index;
  33. }
  34. @Override
  35. public String toString() {
  36. return this.name;
  37. }
  38. public static String getName(int index) {
  39. for (UserType c : UserType.values()) {
  40. if (c.getIndex() == index) {
  41. return c.getName();
  42. }
  43. }
  44. return null;
  45. }
  46. public String getName() {
  47. return name;
  48. }
  49. public int getIndex() {
  50. return index;
  51. }
  52. }
  53. public static JWTUserDetail fromJson(String json) throws JsonProcessingException {
  54. return mapper.readValue(json,JWTUserDetail.class);//JSONObject.parseObject(json, JWTUserDetail.class);
  55. }
  56. public String toJson() throws JsonProcessingException {
  57. return mapper.writeValueAsString(this);//.toJSONString(this);
  58. }
  59. }
  • JwtTokenUtil:token工具
  1. @Component
  2. public class JwtTokenUtil implements Serializable {
  3. private static final long serialVersionUID = -5883980282405596071L;
  4. public static final ThreadLocal<JWTUserDetail> LOCAL_USER = new ThreadLocal<>();
  5. public final static String JWT_TOKEN_PREFIX = "jwt:%s:%d";
  6. private final String JWT_LOGIN_NAME = "JWT_LOGIN_NAME";
  7. private final String JWT_LOGIN_TIME = "JWT_LOGIN_TIME";
  8. private final String JWT_LOGIN_USERID = "JWT_LOGIN_USERID";
  9. private final String JWT_LOGIN_USERTYPE = "JWT_LOGIN_USERTYPE";
  10. //签名方式
  11. private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
  12. //密匙
  13. @Value("${jwt.security.secret}")
  14. private String secret;
  15. @Value("${jwt.access_token:#{30*24*60*60}}")
  16. private Long access_token_expiration;
  17. public String getLoginNameFromToken(String token) {
  18. return getClaimsFromToken(token).getSubject();
  19. }
  20. /**
  21. * 根据token 获取用户信息
  22. */
  23. public JWTUserDetail getUserFromToken(String token) {
  24. JWTUserDetail jwtUserDetails = new JWTUserDetail();
  25. Claims claims = getClaimsFromToken(token);
  26. jwtUserDetails.setUserId(claims.get(JWT_LOGIN_USERID, Long.class));
  27. jwtUserDetails.setLoginName(claims.get(JWT_LOGIN_NAME, String.class));
  28. jwtUserDetails.setUserType(Enum.valueOf(JWTUserDetail.UserType.class, (String) claims.get(JWT_LOGIN_USERTYPE)));
  29. jwtUserDetails.setLoginTime(new Date(claims.get(JWT_LOGIN_TIME, Long.class)));
  30. jwtUserDetails.setJwtToken(token);
  31. return jwtUserDetails;
  32. }
  33. /**
  34. * 根据用户信息生成token
  35. *
  36. * @param user
  37. * @return
  38. */
  39. public String generateToken(JWTUserDetail user) {
  40. Map<String, Object> claims = new HashMap<>();
  41. claims.put(JWT_LOGIN_NAME, user.getLoginName());
  42. claims.put(JWT_LOGIN_TIME, user.getLoginTime());
  43. claims.put(JWT_LOGIN_USERID, user.getUserId());
  44. claims.put(JWT_LOGIN_USERTYPE, user.getUserType());
  45. return Jwts.builder()
  46. //一个map 可以资源存放东西进去
  47. .setClaims(claims)
  48. // 用户名写入标题
  49. .setSubject(user.getLoginName())
  50. .setId(UUID.randomUUID().toString())
  51. .setIssuedAt(new Date())
  52. //过期时间
  53. .setExpiration(new Date(System.currentTimeMillis() + access_token_expiration * 1000))
  54. //数字签名
  55. .signWith(SIGNATURE_ALGORITHM, secret)
  56. .compact();
  57. }
  58. /**
  59. * 根据token 获取生成时间
  60. */
  61. public Date getCreatedDateFromToken(String token) {
  62. return getClaimsFromToken(token).getIssuedAt();
  63. }
  64. /**
  65. * 根据token 获取过期时间
  66. */
  67. public Date getExpirationDateFromToken(String token) {
  68. return getClaimsFromToken(token).getExpiration();
  69. }
  70. /**
  71. * token 是否过期
  72. */
  73. public Boolean isTokenExpired(String token) {
  74. return getExpirationDateFromToken(token).before(new Date());
  75. }
  76. /***
  77. * 解析token 信息
  78. * @param token
  79. * @return
  80. */
  81. private Claims getClaimsFromToken(String token) {
  82. return Jwts.parser()
  83. //签名的key
  84. .setSigningKey(secret)
  85. // 签名token
  86. .parseClaimsJws(token)
  87. .getBody();
  88. }
  89. }
  • SkipPathAntMatcher:token校验的放行策略
  1. @Slf4j
  2. public class SkipPathAntMatcher implements RequestMatcher {
  3. private List<String> pathsToSkip;
  4. public SkipPathAntMatcher(List<String> pathsToSkip) {
  5. this.pathsToSkip = pathsToSkip;
  6. }
  7. @Override
  8. public boolean matches(HttpServletRequest request) {
  9. if (!ObjectUtils.isEmpty(pathsToSkip)) {
  10. for (String s : pathsToSkip) {
  11. AntPathRequestMatcher antPathRequestMatcher = new AntPathRequestMatcher(s);
  12. if (antPathRequestMatcher.matches(request)) {
  13. return true;
  14. }
  15. }
  16. }
  17. return false;
  18. }
  19. }

五、总结

  • 放行策略有两个地方一定要都配置,1.security要配置,2.token的过滤器也要配置。
  • security是校验URL是否有权限访问或者直接放行,我这里没有写用户角色和权限,是因为后面我计划给不同的角色返回不同的菜单,通过菜单来区分。如果你的业务需要可以单独设置
  • token过滤器是配置这次请求是否token有效。security放行的,token也要放行。比如登录和注册页面,这时用户没有任何权限的,所以都放行。

最后,欢迎大家关注我的个人公众号,我会把经历分享出来,助你了解圈内圈外事。

同时也欢迎大家添加个人微信【shishuai860505】,我拉大家进我的读者交流群。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/546377
推荐阅读
相关标签
  

闽ICP备14008679号