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

最近项目的登录验证部分,采用了 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 三个包。

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
 <groupId>com.fasterxml.jackson.core</groupId>
 <artifactId>jackson-core</artifactId>
 <version>2.12.1</version>
</dependency>
<dependency>
 <groupId>com.auth0</groupId>
 <artifactId>java-jwt</artifactId>
 <version>3.12.1</version>
</dependency>

配置 DAO 数据层

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

User

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

@Data
public class User implements UserDetails {

 private static final long serialVersionUID = 1L;

 private String username;

 private String password;

 private Collection<? extends GrantedAuthority> authorities;

 ...
}

UserService

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

@Service
public class UserService implements UserDetailsService {

 @Autowired
 UserMapper userMapper;

 @Override
 @Transactional
 public User loadUserByUsername(String username) {
  return userMapper.getByUsername(username);
 }

 ...
}

创建 JWT 工具类

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

@Component
public class JwtTokenProvider {

 private static final long JWT_EXPIRATION = 5 * 60 * 1000L; // 五分钟过期

 public static final String TOKEN_PREFIX = "Bearer "; // token 的开头字符串

 private String jwtSecret = "XXX 密钥,打死也不能告诉别人";

 ...
}

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

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

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

public boolean validateToken(String authToken) {
 try {
  Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 和签发保持一致
  JWTVerifier verifier = JWT.require(algorithm).build();
  verifier.verify(authToken);
  return true;
 } catch (JWTVerificationException jwtVerificationException) {
  return false;
 }
}

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

public String getUsernameFromJWT(String authToken) {
 try {
  DecodedJWT jwt = JWT.decode(authToken);
  return jwt.getClaim("username").asString();
 } catch (JWTDecodeException jwtDecodeException) {
  return null;
 }
}

登录

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

LoginFilter

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

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

 @Override
 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  if (!request.getMethod().equals("POST")) {
   throw new AuthenticationServiceException("登录接口方法不支持: " + request.getMethod());
  }
  if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
   Map<String, String> loginData = new HashMap<>();
   try {
    loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
   } catch (IOException e) {
   }
   String username = loginData.get(getUsernameParameter());
   String password = loginData.get(getPasswordParameter());
   if (username == null) {
    username = "";
   }
   if (password == null) {
    password = "";
   }
   username = username.trim();
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
     password);
   setDetails(request, authRequest);
   return this.getAuthenticationManager().authenticate(authRequest);
  } else {
   return super.attemptAuthentication(request, response);
  }
 }

}

LoginSuccessHandler

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

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

 @Autowired
 private JwtTokenProvider jwtTokenProvider;

 @Override
 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
   Authentication authentication) throws IOException, ServletException {

  ResponseData responseData = new ResponseData();
  String token = jwtTokenProvider.generateToken(authentication);
  responseData.setData(JwtTokenProvider.TOKEN_PREFIX + token);
  response.setContentType("application/json;charset=utf-8");
  ObjectMapper mapper = new ObjectMapper();
  mapper.writeValue(response.getWriter(), responseData);
 }

}

LoginFailureHandler

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

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

 @Override
 public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
   AuthenticationException exception) throws IOException, ServletException {
  response.setContentType("application/json;charset=utf-8");
  ResponseData respBean = setResponseData(exception);
  ObjectMapper mapper = new ObjectMapper();
  mapper.writeValue(response.getWriter(), respBean);
 }

 private ResponseData setResponseData(AuthenticationException exception) {
  if (exception instanceof LockedException) {
   return ResponseData.build("用户已被锁定");
  } else if (exception instanceof CredentialsExpiredException) {
   return ResponseData.build("密码已过期");
  } else if (exception instanceof AccountExpiredException) {
   return ResponseData.build("用户名已过期");
  } else if (exception instanceof DisabledException) {
   return ResponseData.build("账户不可用");
  } else if (exception instanceof BadCredentialsException) {
   return ResponseData.build("验证失败");
  }
  return ResponseData.build("登录失败,请联系管理员");
 }

}

验证

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

JwtAuthenticationFilter

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

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

 @Autowired
 private JwtTokenProvider jwtProvider;

 @Override
 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
   throws ServletException, IOException {
  try {
   String jwt = getJwtFromRequest(request);
   UsernamePasswordAuthenticationToken authentication = verifyToken(jwt);
   if (authentication != null) {
    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
   }
   SecurityContextHolder.getContext().setAuthentication(authentication);
  } catch (Exception e) {
   logger.error("无法给 Security 上下文设置用户验证对象", e);
  }

  filterChain.doFilter(request, response);
 }

 private String getJwtFromRequest(HttpServletRequest request) {
  String bearerToken = request.getHeader("Authorization");
  if (bearerToken == null || !bearerToken.startsWith(JwtTokenProvider.TOKEN_PREFIX)) {
   logger.info("请求头不含 JWT token,调用下个过滤器");
   return null;
  }

  return bearerToken.split(" ")[1].trim();
 }

 // 验证token,并生成认证后的token
 private UsernamePasswordAuthenticationToken verifyToken(String token) {
  if (token == null) {
   return null;
  }
  // 认证失败,返回null
  if (!jwtProvider.validateToken(token)) {
   return null;
  }
  // 提取用户名
  String username = jwtProvider.getUsernameFromJWT(token);
  UserDetails userDetails = new User(username);

  // 构建认证过的token
  return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
 }
}

AuthenticationEntryPoint

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

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

 private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);

 @Override
 public void commence(HttpServletRequest request, HttpServletResponse response,
   AuthenticationException authException) throws IOException, ServletException {
  logger.error("验证为通过. 提示信息 - {}", authException.getMessage());
  response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
 }

}

集中配置

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

@Configuration
@EnableWebSecurity
public class KanpmSecurityConfig extends WebSecurityConfigurerAdapter {

 @Autowired
 UserDetailsService userDetailsService;

 @Autowired
 private JwtAuthenticationEntryPoint unauthorizedHandler;

 @Autowired
 private JwtAuthenticationFilter jwtAuthenticationFilter;

 @Bean
 public LoginFilter loginFilter(LoginSuccessHandler loginSuccessHandler, LoginFailureHandler loginFailureHandler)
   throws Exception {
  LoginFilter loginFilter = new LoginFilter();
  loginFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
  loginFilter.setAuthenticationFailureHandler(loginFailureHandler);
  loginFilter.setAuthenticationManager(authenticationManagerBean());
  loginFilter.setFilterProcessesUrl("/auth/login");
  return loginFilter;
 }

 @Bean
 @Override
 public AuthenticationManager authenticationManagerBean() throws Exception {
  return super.authenticationManagerBean();
 }

 @Bean
 public PasswordEncoder passwordEncoder() {
  return new BCryptPasswordEncoder();
 }

 ...
}

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

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

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

@Override
protected void configure(HttpSecurity http) throws Exception {
 http.csrf().disable().anyRequest().authenticated().and()
   .exceptionHandling().authenticationEntryPoint(unauthorizedHandler);

 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
 .addFilterAt(loginFilter(new LoginSuccessHandler(), new LoginFailureHandler()),
   UsernamePasswordAuthenticationFilter.class);
}

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

(0)

相关推荐

  • SpringSecurity Jwt Token 自动刷新的实现

    功能需求 最近项目中有这么一个功能,用户登录系统后,需要给 用户 颁发一个 token ,后续访问系统的请求都需要带上这个 token ,如果请求没有带上这个 token 或者 token 过期了,那么禁止访问系统.如果用户一直访问系统,那么还需要自动延长 token 的过期时间. 功能分析 1.token 的生成 使用现在比较流行的 jwt 来生成. 2.token 的自动延长 要实现 token 的自动延长,系统给用户 颁发 一个 token 无法实现,那么通过变通一个,给用户生成 2个 t

  • Spring Security基于JWT实现SSO单点登录详解

    SSO :同一个帐号在同一个公司不同系统上登陆 使用SpringSecurity实现类似于SSO登陆系统是十分简单的 下面我就搭建一个DEMO 首先来看看目录的结构 其中sso-demo是父工程项目 sso-client .sso-client2分别对应2个资源服务器,sso-server是认证服务器 引入的pom文件 sso-demo <?xml version="1.0" encoding="UTF-8"?> <project xmlns=&q

  • Spring Security代码实现JWT接口权限授予与校验功能

    通过笔者前两篇文章的说明,相信大家已经知道JWT是什么,怎么用,该如何结合Spring Security使用.那么本节就用代码来具体的实现一下JWT登录认证及鉴权的流程. 一.环境准备工作 建立Spring Boot项目并集成了Spring Security,项目可以正常启动 通过controller写一个HTTP的GET方法服务接口,比如:"/hello" 实现最基本的动态数据验证及权限分配,即实现UserDetailsService接口和UserDetails接口.这两个接口都是向

  • 详解SpringBoot+SpringSecurity+jwt整合及初体验

    原来一直使用shiro做安全框架,配置起来相当方便,正好有机会接触下SpringSecurity,学习下这个.顺道结合下jwt,把安全信息管理的问题扔给客户端, 准备 首先用的是SpringBoot,省去写各种xml的时间.然后把依赖加入一下 <!--安全--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-secu

  • Spring Security OAuth2实现使用JWT的示例代码

    1.概括 在博客中,我们将讨论如何让Spring Security OAuth2实现使用JSON Web Tokens. 2.Maven 配置 首先,我们需要在我们的pom.xml中添加spring-security-jwt依赖项. <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> &l

  • SpringBoot Security整合JWT授权RestAPI的实现

    本教程主要详细讲解SpringBoot Security整合JWT授权RestAPI. 基础环境 技术 版本 Java 1.8+ SpringBoot 2.x.x Security 5.x JWT 0.9.0 创建项目 初始化项目 mvn archetype:generate -DgroupId=com.edurt.sli.slisj -DartifactId=spring-learn-integration-security-jwt -DarchetypeArtifactId=maven-ar

  • Spring Boot(四)之使用JWT和Spring Security保护REST API

    通常情况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就喝一壶的.那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API.目前,比较主流的方案有几种: 用户名和密码鉴权,使用Session保存用户鉴权结果. 使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式) 自行采用Token进行鉴权 第一种就不介绍了,由于依赖Session来维护状态,也不太适合移动时代,新的项目就不要采用了.

  • SpringSecurity整合Jwt过程图解

    这篇文章主要介绍了SpringSecurity整合Jwt过程图解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 一.创建项目并导入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

  • SpringBoot+Spring Security+JWT实现RESTful Api权限控制的方法

    摘要:用spring-boot开发RESTful API非常的方便,在生产环境中,对发布的API增加授权保护是非常必要的.现在我们来看如何利用JWT技术为API增加授权保护,保证只有获得授权的用户才能够访问API. 一:开发一个简单的API 在IDEA开发工具中新建一个maven工程,添加对应的依赖如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-b

  • Spring Security结合JWT的方法教程

    概述 众所周知使用 JWT 做权限验证,相比 Session 的优点是,Session 需要占用大量服务器内存,并且在多服务器时就会涉及到共享 Session 问题,在手机等移动端访问时比较麻烦 而 JWT 无需存储在服务器,不占用服务器资源(也就是无状态的),用户在登录后拿到 Token 后,访问需要权限的请求时附上 Token(一般设置在Http请求头),JWT 不存在多服务器共享的问题,也没有手机移动端访问问题,若为了提高安全,可将 Token 与用户的 IP 地址绑定起来 前端流程 用户

随机推荐