SpringSecurity5(9-JWT整合)
|总字数:3.1k|阅读时长:16分钟|浏览量:|
依赖配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
   | <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency>     <groupId>org.apache.commons</groupId>     <artifactId>commons-pool2</artifactId>     <version>2.9.0</version> </dependency> <dependency>     <groupId>io.jsonwebtoken</groupId>     <artifactId>jjwt</artifactId>     <version>0.9.1</version> </dependency> <dependency>     <groupId>com.github.axet</groupId>     <artifactId>kaptcha</artifactId>     <version>0.0.9</version> </dependency> <dependency>     <groupId>cn.hutool</groupId>     <artifactId>hutool-all</artifactId>     <version>5.7.15</version> </dependency> <dependency>     <groupId>org.apache.commons</groupId>     <artifactId>commons-lang3</artifactId> </dependency> <dependency>     <groupId>commons-codec</groupId>     <artifactId>commons-codec</artifactId> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency>     <groupId>org.projectlombok</groupId>     <artifactId>lombok</artifactId> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
   | 
 
自定义全局返回结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
   | @Data public class Result implements Serializable {     private int code;     private String msg;     private Object data;
      public static Result succ(Object data) {         return succ(200, "操作成功", data);     }
      public static Result fail(String msg) {         return fail(400, msg, null);     }
      public static Result succ (int code, String msg, Object data) {         Result result = new Result();         result.setCode(code);         result.setMsg(msg);         result.setData(data);         return result;     }
      public static Result fail (int code, String msg, Object data) {         Result result = new Result();         result.setCode(code);         result.setMsg(msg);         result.setData(data);         return result;     } }
   | 
 
JWT 配置类
1 2 3 4
   | jwt:   header: Authorization   expire: 604800     secret: 123456
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
   | @Data @Component @ConfigurationProperties(prefix = "jwt") public class JwtUtils {          private long expire;     private String secret;     private String header;
      
 
 
 
      public String generateToken(String username){         Date nowDate = new Date();         Date expireDate = new Date(nowDate.getTime() + 1000 * expire);                  return Jwts.builder()                 .setHeaderParam("typ","JWT")                 .setSubject(username)                 .setIssuedAt(nowDate)                 .setExpiration(expireDate)                   .signWith(SignatureAlgorithm.HS512,secret)                 .compact();     }
      
 
 
 
      public Claims getClaimsByToken(String jwt){         try {             return Jwts.parser()                     .setSigningKey(secret)                     .parseClaimsJws(jwt)                     .getBody();         }catch (Exception e){             return null;         }     }
      
 
 
 
      public boolean isTokenExpired(Claims claims){         return claims.getExpiration().before(new Date());     } }
   | 
 
自定义登录处理器
登录失败后,我们需要向前端发送错误信息,登录成功后,我们需要生成 JWT,并将 JWT 返回给前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | 
 
  @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler {
      @Autowired     private JwtUtils jwtUtils;
      @Override     public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {         httpServletResponse.setContentType("application/json;charset=UTF-8");         ServletOutputStream outputStream = httpServletResponse.getOutputStream();
                   String jwt = jwtUtils.generateToken(authentication.getName());         httpServletResponse.setHeader(jwtUtils.getHeader(), jwt);
          Result result = Result.succ("SuccessLogin");         outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));         outputStream.flush();         outputStream.close();     } }
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | 
 
  @Component public class LoginFailureHandler implements AuthenticationFailureHandler {
      @Override     public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {         httpServletResponse.setContentType("application/json;charset=UTF-8");         ServletOutputStream outputStream = httpServletResponse.getOutputStream();
          String errorMessage = "用户名或密码错误";         Result result;         if (e instanceof CaptchaException) {             errorMessage = "验证码错误";             result = Result.fail(errorMessage);         } else {             result = Result.fail(errorMessage);         }         outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));         outputStream.flush();         outputStream.close();     } }
 
  | 
 
自定义登出处理器
在用户退出登录时,我们需将原来的 JWT 置为空返给前端,这样前端会将空字符串覆盖之前的 jwt,JWT 是无状态化的,销毁 JWT 是做不到的,JWT 生成之后,只有等 JWT 过期之后,才会失效。因此我们采取置空策略来清除浏览器中保存的 JWT。同时我们还要将我们之前置入 SecurityContext 中的用户信息进行清除,这可以通过创建 SecurityContextLogoutHandler 对象,调用它的 logout 方法完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   | 
 
  @Component public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {          @Autowired     private JwtUtils jwtUtils;          @Override     public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {         if (authentication!=null){             new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);         }         httpServletResponse.setContentType("application/json;charset=UTF-8");         ServletOutputStream outputStream = httpServletResponse.getOutputStream();                  httpServletResponse.setHeader(jwtUtils.getHeader(), "");
          Result result = Result.succ("SuccessLogout");                  outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));         outputStream.flush();         outputStream.close();     } }
 
  | 
 
验证码配置
1 2 3 4 5 6
   | public class CaptchaException extends AuthenticationException {          public CaptchaException(String msg){         super(msg);     } }
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | 
 
  @Configuration public class KaptchaConfig {          @Bean     public DefaultKaptcha producer(){         Properties properties = new Properties();         properties.put("kaptcha.border", "no");         properties.put("kaptcha.textproducer.font.color", "black");         properties.put("kaptcha.textproducer.char.space", "4");         properties.put("kaptcha.image.height", "40");         properties.put("kaptcha.image.width", "120");         properties.put("kaptcha.textproducer.font.size", "30");         Config config = new Config(properties);         DefaultKaptcha defaultKaptcha = new DefaultKaptcha();         defaultKaptcha.setConfig(config);         return defaultKaptcha;     } }
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | @RestController public class CaptchaController {
      @Autowired     private Producer producer;     @Autowired     private RedisUtil redisUtil;
      @GetMapping("/captcha")     public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {         String code = producer.createText();         BufferedImage image = producer.createImage(code);
          redisUtil.set("captcha", code, 120);
                   response.setHeader("Cache-Control", "no-store");         response.setHeader("Pragma", "no-cache");         response.setDateHeader("Expires", 0);         response.setContentType("image/jpeg");         ImageIO.write(image, "jpg", response.getOutputStream());     } }
   | 
 
自定义过滤器
OncePerRequestFilter:在每次请求时只执行一次过滤,保证一次请求只通过一次 filter,而不需要重复执行
因为验证码是一次性使用的,一个验证码对应一个用户的一次登录过程,所以需用 hdel 将存储的 key 删除。当校验失败时,则交给登录认证失败处理器 LoginFailureHandler 进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
   | 
 
  @Component public class CaptchaFilter extends OncePerRequestFilter {
      @Autowired     private RedisUtil redisUtil;     @Autowired     private LoginFailureHandler loginFailureHandler;
      @Override     protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {         String url = httpServletRequest.getRequestURI();         if ("/login/form".equals(url) && httpServletRequest.getMethod().equals("POST")) {                          try {                 validate(httpServletRequest);             } catch (CaptchaException e) {                                  loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);                 return;             }         }         filterChain.doFilter(httpServletRequest, httpServletResponse);     }
      private void validate(HttpServletRequest request) {         String code = request.getParameter("code");
          if (StringUtils.isBlank(code)) {             throw new CaptchaException("验证码错误");         }         String captcha = (String) redisUtil.get("captcha");         if (!code.equals(captcha)) {             throw new CaptchaException("验证码错误");         }
                   redisUtil.del("captcha");     } }
 
  | 
 
login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head>     <meta charset="UTF-8">     <title>登录</title> </head> <body> <h3>表单登录</h3> <form method="post" th:action="@{/login/form}">     <input type="text" name="username" placeholder="用户名"><br>     <input type="password" name="password" placeholder="密码"><br>     <input type="text" name="code" placeholder="验证码"><br>     <img th:onclick="this.src='/captcha?'+Math.random()" th:src="@{/captcha}" alt="验证码"/><br>     <div th:if="${param.error}">         <span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span>     </div>     <button type="submit">登录</button> </form> </body> </html>
   | 
 
BasicAuthenticationFilter:OncePerRequestFilter 执行完后,由 BasicAuthenticationFilter 检测和处理 http basic 认证,取出请求头中的 jwt,校验 jwt
- 
当前端发来的请求有 JWT 信息时,该过滤器将检验 JWT 是否正确以及是否过期,若检验成功,则获取 JWT 中的用户名信息,检索数据库获得用户实体类,并将用户信息告知 Spring Security,后续我们就能调用 security 的接口获取到当前登录的用户信息。
 
- 
若前端发的请求不含 JWT,我们也不能拦截该请求,因为一般的项目都是允许匿名访问的,有的接口允许不登录就能访问,没有 JWT 也放行是安全的,因为我们可以通过 Spring Security 进行权限管理,设置一些接口需要权限才能访问,不允许匿名访问
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
   | 
 
  public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
      @Autowired     private JwtUtils jwtUtils;     @Autowired     private UserDetailServiceImpl userDetailService;
      public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {         super(authenticationManager);     }
      @Override     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {         String jwt = request.getHeader(jwtUtils.getHeader());                           if (StrUtil.isBlankOrUndefined(jwt)) {             chain.doFilter(request, response);             return;         }
          Claims claim = jwtUtils.getClaimsByToken(jwt);         if (claim == null) {             throw new JwtException("token异常");         }         if (jwtUtils.isTokenExpired(claim)) {             throw new JwtException("token已过期");         }         String username = claim.getSubject();         User user = UserDetailServiceImpl.userMap.get(username);
                   UsernamePasswordAuthenticationToken token =                 new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(user.getId()));         SecurityContextHolder.getContext().setAuthentication(token);         chain.doFilter(request, response);     } }
 
  | 
 
自定义权限异常处理器
当 BasicAuthenticationFilter 认证失败的时候会进入 AuthenticationEntryPoint
我们之前放行了匿名请求,但有的接口是需要权限的,当用户权限不足时,会进入 AccessDenieHandler 进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | 
 
  @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {          @Override     public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {         httpServletResponse.setContentType("application/json;charset=UTF-8");         httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);         ServletOutputStream outputStream = httpServletResponse.getOutputStream();
          Result result = Result.fail("请先登录");         outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));         outputStream.flush();         outputStream.close();     } }
 
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | 
 
  @Component public class JwtAccessDeniedHandler implements AccessDeniedHandler {          @Override     public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {         httpServletResponse.setContentType("application/json;charset=UTF-8");         httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);         ServletOutputStream outputStream = httpServletResponse.getOutputStream();
          Result result = Result.fail(e.getMessage());                  outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));         outputStream.flush();         outputStream.close();     } }
 
  | 
 
自定义用户登录逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
   | public class AccountUser implements UserDetails {
      private Long userId;
      private static final long serialVersionUID = 540L;     private static final Log logger = LogFactory.getLog(User.class);     private String password;     private final String username;     private final Collection<? extends GrantedAuthority> authorities;     private final boolean accountNonExpired;     private final boolean accountNonLocked;     private final boolean credentialsNonExpired;     private final boolean enabled;
      public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {         this(userId, username, password, true, true, true, true, authorities);     }
      public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {         Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");         this.userId = userId;         this.username = username;         this.password = password;         this.enabled = enabled;         this.accountNonExpired = accountNonExpired;         this.credentialsNonExpired = credentialsNonExpired;         this.accountNonLocked = accountNonLocked;         this.authorities = authorities;     }
      @Override     public Collection<? extends GrantedAuthority> getAuthorities() {         return this.authorities;     }
      @Override     public String getPassword() {         return this.password;     }
      @Override     public String getUsername() {         return this.username;     }
      @Override     public boolean isAccountNonExpired() {         return this.accountNonExpired;     }
      @Override     public boolean isAccountNonLocked() {         return this.accountNonLocked;     }
      @Override     public boolean isCredentialsNonExpired() {         return this.credentialsNonExpired;     }
      @Override     public boolean isEnabled() {         return this.enabled;     } }
  | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
   | @Service public class UserDetailServiceImpl implements UserDetailsService {
       public static Map<String, User> userMap = new HashMap<>();
      static {         userMap.put("root", new User("root", "123", AuthorityUtils.createAuthorityList("all")));     }
      @Resource     private PasswordEncoder passwordEncoder;
      @Override     public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {         User user = userMap.get(s);         if (user == null) {             throw new UsernameNotFoundException("用户名或密码错误");         }         return new AccountUser(1L, user.getUsername(), passwordEncoder.encode(user.getPassword()), user.getAuthorities());     } }
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
   | @Component public class PasswordEncoder extends BCryptPasswordEncoder {
      
 
 
 
      @Override     public String encode(CharSequence charSequence) {         try {             MessageDigest digest = MessageDigest.getInstance("MD5");             return toHexString(digest.digest(charSequence.toString().getBytes()));         } catch (NoSuchAlgorithmException e) {             e.printStackTrace();             return "";         }     }
      
 
 
 
 
      @Override     public boolean matches(CharSequence charSequence, String s) {         return s.equals(encode(charSequence));     }
      
 
 
      private String toHexString(byte [] tmp){         StringBuilder builder = new StringBuilder();         for (byte b :tmp){             String s = Integer.toHexString(b & 0xFF);             if (s.length()==1){                 builder.append("0");             }             builder.append(s);         }         return builder.toString();     } }
   | 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
   | @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter {
      @Autowired     LoginFailureHandler loginFailureHandler;     @Autowired     LoginSuccessHandler loginSuccessHandler;     @Autowired     CaptchaFilter captchaFilter;     @Autowired     JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;     @Autowired     JwtAccessDeniedHandler jwtAccessDeniedHandler;     @Autowired     UserDetailServiceImpl userDetailService;     @Autowired     JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
      @Bean     JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {         JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());         return jwtAuthenticationFilter;     }
      private static final String[] URL_WHITELIST = {             "/login",             "/logout",             "/captcha",             "/favicon.ico"     };
      @Bean     public PasswordEncoderImpl passwordEncoder() {         return new PasswordEncoderImpl();     }
      @Override     protected void configure(HttpSecurity http) throws Exception {         http.cors()             .and()             .csrf().disable()                          .formLogin()             .loginPage("/login")             .loginProcessingUrl("/login/form")             .successHandler(loginSuccessHandler)             .failureHandler(loginFailureHandler)
              .and()             .logout()             .logoutSuccessHandler(jwtLogoutSuccessHandler)
                           .and()             .sessionManagement()             .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                           .and()             .authorizeRequests()             .antMatchers(URL_WHITELIST).permitAll()             .anyRequest().authenticated()
                           .and()             .exceptionHandling()             .authenticationEntryPoint(jwtAuthenticationEntryPoint)             .accessDeniedHandler(jwtAccessDeniedHandler)
                           .and()             .addFilter(jwtAuthenticationFilter())                          .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);     }
      @Override     protected void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(userDetailService)             .passwordEncoder(passwordEncoder());     }
      
 
      @Override     public void configure(WebSecurity web) throws Exception {                  web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");     } }
   |