SpringSecurity构建基于JWT的登录认证实现

2025-05-29 0 50

最近项目的登录验证部分,采用了 JWT 验证的方式。并且既然采用了 Spring Boot 框架,验证和权限管理这部分,就自然用了 Spring Security。这里记录一下具体实现。
在项目采用 JWT 方案前,有必要先了解它的特性和适用场景,毕竟软件工程里,没有银弹。只有合适的场景,没有万精油的方案。

一言以蔽之,JWT 可以携带非敏感信息,并具有不可篡改性。可以通过验证是否被篡改,以及读取信息内容,完成网络认证的三个问题:“你是谁”、“你有哪些权限”、“是不是冒充的”。

为了安全,使用它需要采用 Https 协议,并且一定要小心防止用于加密的密钥泄露。

采用 JWT 的认证方式下,服务端并不存储用户状态信息,有效期内无法废弃,有效期到期后,需要重新创建一个新的来替换。
所以它并不适合做长期状态保持,不适合需要用户踢下线的场景,不适合需要频繁修改用户信息的场景。因为要解决这些问题,总是需要额外查询数据库或者缓存,或者反复加密解密,强扭的瓜不甜,不如直接使用 Session。不过作为服务间的短时效切换,还是非常合适的,就比如 OAuth 之类的。

目标功能点

通过填写用户名和密码登录。

  • 验证成功后, 服务端生成 JWT 认证 token, 并返回给客户端。
  • 验证失败后返回错误信息。
  • 客户端在每次请求中携带 JWT 来访问权限内的接口。

每次请求验证 token 有效性和权限,在无有效 token 时抛出 401 未授权错误。
当发现请求带着的 token 有效期快到了的时候,返回特定状态码,重新请求一个新 token。

准备工作

引入 Maven 依赖

针对这个登录验证的实现,需要引入 Spring Security、jackson、java-jwt 三个包。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.fasterxml.jackson.core</groupId>
  7. <artifactId>jackson-core</artifactId>
  8. <version>2.12.1</version>
  9. </dependency>
  10. <dependency>
  11. <groupId>com.auth0</groupId>
  12. <artifactId>java-jwt</artifactId>
  13. <version>3.12.1</version>
  14. </dependency>

配置 DAO 数据层

要验证用户前,自然是先要创建用户实体对象,以及获取用户的服务类。不同的是,这两个类需要实现 Spring Security 的接口,以便将它们集成到验证框架中。

User

用户实体类需要实现 ”UserDetails“ 接口,这个接口要求实现 getUsername、getPassword、getAuthorities 三个方法,用以获取用户名、密码和权限。以及 isAccountNonExpired“`isAccountNonLocked、isCredentialsNonExpired、isEnabled 这四个判断是否是有效用户的方法,因为和验证无关,所以先都返回 true。这里图方便,用了 lombok。

  1. @Data
  2. public class User implements UserDetails {
  3. private static final long serialVersionUID = 1L;
  4. private String username;
  5. private String password;
  6. private Collection<? extends GrantedAuthority> authorities;
  7. }

UserService

用户服务类需要实现 “UserDetailsService” 接口,这个接口非常简单,只需要实现 loadUserByUsername(String username) 这么一个方法。这里使用了 MyBatis 来连接数据库获取用户信息。

  1. @Service
  2. public class UserService implements UserDetailsService {
  3. @Autowired
  4. UserMapper userMapper;
  5. @Override
  6. @Transactional
  7. public User loadUserByUsername(String username) {
  8. return userMapper.getByUsername(username);
  9. }
  10. }

创建 JWT 工具类

这个工具类主要负责 token 的生成,验证,从中取值。

  1. @Component
  2. public class JwtTokenProvider {
  3. private static final long JWT_EXPIRATION = 5 * 60 * 1000L; // 五分钟过期
  4. public static final String TOKEN_PREFIX = "Bearer "; // token 的开头字符串
  5. private String jwtSecret = "XXX 密钥,打死也不能告诉别人";
  6. }

生成 JWT:从以通过验证的认证对象中,获取用户信息,然后用指定加密方式,以及过期时间生成 token。这里简单的只加了用户名这一个信息到 token 中:

  1. public String generateToken(Authentication authentication) {
  2. User userPrincipal = (User) authentication.getPrincipal(); // 获取用户对象
  3. Date expireDate = new Date(System.currentTimeMillis() + JWT_EXPIRATION); // 设置过期时间
  4. try {
  5. Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 指定加密方式
  6. return JWT.create().withExpiresAt(expireDate).withClaim("username", userPrincipal.getUsername())
  7. .sign(algorithm); // 签发 JWT
  8. } catch (JWTCreationException jwtCreationException) {
  9. return null;
  10. }
  11. }

验证 JWT:指定和签发相同的加密方式,验证这个 token 是否是本服务器签发,是否篡改,或者已过期。

  1. public boolean validateToken(String authToken) {
  2. try {
  3. Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 和签发保持一致
  4. JWTVerifier verifier = JWT.require(algorithm).build();
  5. verifier.verify(authToken);
  6. return true;
  7. } catch (JWTVerificationException jwtVerificationException) {
  8. return false;
  9. }
  10. }

获取荷载信息:从 token 的荷载部分里解析用户名信息,这部分是 md5 编码的,属于公开信息。

  1. public String getUsernameFromJWT(String authToken) {
  2. try {
  3. DecodedJWT jwt = JWT.decode(authToken);
  4. return jwt.getClaim("username").asString();
  5. } catch (JWTDecodeException jwtDecodeException) {
  6. return null;
  7. }
  8. }

登录

登录部分需要创建三个文件:负责登录接口处理的拦截器,登陆成功或者失败的处理类。

LoginFilter

Spring Security 默认自带表单登录,负责处理这个登录验证过程的过滤器叫“UsernamePasswordAuthenticationFilter”,不过它只支持表单传值,这里用自定义的类继承它,使其能够支持 JSON 传值,负责登录验证接口。
这个拦截器只需要负责从请求中取值即可,验证工作 Spring Security 会帮我们处理好。

  1. public class LoginFilter extends UsernamePasswordAuthenticationFilter {
  2. @Override
  3. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  4. if (!request.getMethod().equals("POST")) {
  5. throw new AuthenticationServiceException("登录接口方法不支持: " + request.getMethod());
  6. }
  7. if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
  8. Map<String, String> loginData = new HashMap<>();
  9. try {
  10. loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
  11. } catch (IOException e) {
  12. }
  13. String username = loginData.get(getUsernameParameter());
  14. String password = loginData.get(getPasswordParameter());
  15. if (username == null) {
  16. username = "";
  17. }
  18. if (password == null) {
  19. password = "";
  20. }
  21. username = username.trim();
  22. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
  23. password);
  24. setDetails(request, authRequest);
  25. return this.getAuthenticationManager().authenticate(authRequest);
  26. } else {
  27. return super.attemptAuthentication(request, response);
  28. }
  29. }
  30. }

LoginSuccessHandler

负责在登录成功后,生成 JWT 给前端。

  1. @Component
  2. public class LoginSuccessHandler implements AuthenticationSuccessHandler {
  3. @Autowired
  4. private JwtTokenProvider jwtTokenProvider;
  5. @Override
  6. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
  7. Authentication authentication) throws IOException, ServletException {
  8. ResponseData responseData = new ResponseData();
  9. String token = jwtTokenProvider.generateToken(authentication);
  10. responseData.setData(JwtTokenProvider.TOKEN_PREFIX + token);
  11. response.setContentType("application/json;charset=utf-8");
  12. ObjectMapper mapper = new ObjectMapper();
  13. mapper.writeValue(response.getWriter(), responseData);
  14. }
  15. }

LoginFailureHandler

验证失败后,返回错误信息。

  1. @Component
  2. public class LoginFailureHandler implements AuthenticationFailureHandler {
  3. @Override
  4. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
  5. AuthenticationException exception) throws IOException, ServletException {
  6. response.setContentType("application/json;charset=utf-8");
  7. ResponseData respBean = setResponseData(exception);
  8. ObjectMapper mapper = new ObjectMapper();
  9. mapper.writeValue(response.getWriter(), respBean);
  10. }
  11. private ResponseData setResponseData(AuthenticationException exception) {
  12. if (exception instanceof LockedException) {
  13. return ResponseData.build("用户已被锁定");
  14. } else if (exception instanceof CredentialsExpiredException) {
  15. return ResponseData.build("密码已过期");
  16. } else if (exception instanceof AccountExpiredException) {
  17. return ResponseData.build("用户名已过期");
  18. } else if (exception instanceof DisabledException) {
  19. return ResponseData.build("账户不可用");
  20. } else if (exception instanceof BadCredentialsException) {
  21. return ResponseData.build("验证失败");
  22. }
  23. return ResponseData.build("登录失败,请联系管理员");
  24. }
  25. }

验证

在成功登陆后,前端在每次发起请求时携带签发的 JWT,让服务端能识别这是已登录的用户。
同时,如果未携带 JWT,或携带的 token 过期,或者非法,用单独的处理类返回错误信息。

JwtAuthenticationFilter

负责在每次请求中,解析请求头中的 JWT,从中取得用户信息,生成验证对象传递给下一个过滤器。

  1. @Component
  2. public class JwtAuthenticationFilter extends OncePerRequestFilter {
  3. @Autowired
  4. private JwtTokenProvider jwtProvider;
  5. @Override
  6. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
  7. throws ServletException, IOException {
  8. try {
  9. String jwt = getJwtFromRequest(request);
  10. UsernamePasswordAuthenticationToken authentication = verifyToken(jwt);
  11. if (authentication != null) {
  12. authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
  13. }
  14. SecurityContextHolder.getContext().setAuthentication(authentication);
  15. } catch (Exception e) {
  16. logger.error("无法给 Security 上下文设置用户验证对象", e);
  17. }
  18. filterChain.doFilter(request, response);
  19. }
  20. private String getJwtFromRequest(HttpServletRequest request) {
  21. String bearerToken = request.getHeader("Authorization");
  22. if (bearerToken == null || !bearerToken.startsWith(JwtTokenProvider.TOKEN_PREFIX)) {
  23. logger.info("请求头不含 JWT token,调用下个过滤器");
  24. return null;
  25. }
  26. return bearerToken.split(" ")[1].trim();
  27. }
  28. // 验证token,并生成认证后的token
  29. private UsernamePasswordAuthenticationToken verifyToken(String token) {
  30. if (token == null) {
  31. return null;
  32. }
  33. // 认证失败,返回null
  34. if (!jwtProvider.validateToken(token)) {
  35. return null;
  36. }
  37. // 提取用户名
  38. String username = jwtProvider.getUsernameFromJWT(token);
  39. UserDetails userDetails = new User(username);
  40. // 构建认证过的token
  41. return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
  42. }
  43. }

AuthenticationEntryPoint

这个类就比较简单,只是在验证不通过后,返回 401 响应,并记录错误信息。

  1. @Component
  2. public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
  3. private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
  4. @Override
  5. public void commence(HttpServletRequest request, HttpServletResponse response,
  6. AuthenticationException authException) throws IOException, ServletException {
  7. logger.error("验证为通过. 提示信息 – {}", authException.getMessage());
  8. response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
  9. }
  10. }

集中配置

Spring Security 的功能是通过一系列的过滤器链实现的,而配置整个 Spring Security,只需要统一在一个类中配置即可。
现在咱们就创建这个类,继承自 “WebSecurityConfigurerAdapter”,把上面准备好的各种文件,一一配置进去。
首先是通过注解,设置打开全局的 Spring Security 功能,并通过依赖注入,引入刚刚创建的类。

  1. @Configuration
  2. @EnableWebSecurity
  3. public class KanpmSecurityConfig extends WebSecurityConfigurerAdapter {
  4. @Autowired
  5. UserDetailsService userDetailsService;
  6. @Autowired
  7. private JwtAuthenticationEntryPoint unauthorizedHandler;
  8. @Autowired
  9. private JwtAuthenticationFilter jwtAuthenticationFilter;
  10. @Bean
  11. public LoginFilter loginFilter(LoginSuccessHandler loginSuccessHandler, LoginFailureHandler loginFailureHandler)
  12. throws Exception {
  13. LoginFilter loginFilter = new LoginFilter();
  14. loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
  15. loginFilter.setAuthenticationFailureHandler(loginFailureHandler);
  16. loginFilter.setAuthenticationManager(authenticationManagerBean());
  17. loginFilter.setFilterProcessesUrl("/auth/login");
  18. return loginFilter;
  19. }
  20. @Bean
  21. @Override
  22. public AuthenticationManager authenticationManagerBean() throws Exception {
  23. return super.authenticationManagerBean();
  24. }
  25. @Bean
  26. public PasswordEncoder passwordEncoder() {
  27. return new BCryptPasswordEncoder();
  28. }
  29. }

接着,再把用户获取服务类和加密方式,配置到 Spring Security 中去,让它知道如何去验证登录。

  1. @Override
  2. public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
  3. authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  4. }

最后,将JWT过滤器放入过滤器链中,用自定义的登录过滤器替代默认的 “UsernamePasswordAuthenticationFilter”,完成功能。

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. http.csrf().disable().anyRequest().authenticated().and()
  4. .exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
  5. http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
  6. .addFilterAt(loginFilter(new LoginSuccessHandler(), new LoginFailureHandler()),
  7. UsernamePasswordAuthenticationFilter.class);
  8. }

到此这篇关于SpringSecurity构建基于JWT登录认证实现的文章就介绍到这了,更多相关SpringSecurity JWT登录认证内容请搜索快网idc以前的文章或继续浏览下面的相关文章希望大家以后多多支持快网idc!

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 SpringSecurity构建基于JWT的登录认证实现 https://www.kuaiidc.com/109335.html

相关文章

发表评论
暂无评论