通过笔者前两篇文章的说明, 相信大家已经知道 JWT 是什么, 怎么用, 该如何结合 Spring Security 使用. 那么本节就用代码来具体的实现一下 JWT 登录认证及鉴权的流程.
一, 环境准备工作
建立 Spring Boot 项目并集成了 Spring Security, 项目可以正常启动
通过 controller 写一个 HTTP 的 GET 方法服务接口, 比如:"/hello"
实现最基本的动态数据验证及权限分配, 即实现 UserDetailsService 接口和 UserDetails 接口. 这两个接口都是向 Spring Security 提供用户, 角色, 权限等校验信息的接口
如果你学习过 Spring Security 的 formLogin 登录模式, 请将 HttpSecurity 配置中的 formLogin()配置段全部去掉. 因为 JWT 完全使用 JSON 接口, 没有 from 表单提交.
HttpSecurity 配置中一定要加上 csrf().disable(), 即暂时关掉跨站攻击 CSRF 的防御. 这样是不安全的, 我们后续章节再做处理.
以上的内容, 我们在之前的文章中都已经讲过. 如果仍然不熟悉, 可以翻看本号之前的文章.
## 二, 开发 JWT 工具类
通过 maven 坐标引入 JWT 工具包 jjwt
- <dependency>
- <groupId>io.jsonwebtoken</groupId>
- <artifactId>jjwt</artifactId>
- <version>0.9.0</version>
- </dependency>
在 application.YAML 中加入如下自定义一些关于 JWT 的配置
- jwt:
- header: JWTHeaderName
- secret: aabbccdd
- expiration: 3600000
其中 header 是携带 JWT 令牌的 HTTP 的 Header 的名称. 虽然我这里叫做 JWTHeaderName, 但是在实际生产中可读性越差越安全.
secret 是用来为 JWT 基础信息加密和解密的密钥. 虽然我在这里在配置文件写死了, 但是在实际生产中通常不直接写在配置文件里面. 而是通过应用的启动参数传递, 并且需要定期修改.
expiration 是 JWT 令牌的有效时间.
写一个 Spring Boot 配置自动加载的工具类.
- @Data
- @ConfigurationProperties(prefix = "jwt") // 配置自动加载, prefix 是配置的前缀
- @Component
- public class JwtTokenUtil implements Serializable {
- private String secret;
- private Long expiration;
- private String header;
- /**
- * 生成 token 令牌
- *
- * @param userDetails 用户
- * @return 令 token 牌
- */
- public String generateToken(UserDetails userDetails) {
- Map<String, Object> claims = new HashMap<>(2);
- claims.put("sub", userDetails.getUsername());
- claims.put("created", new Date());
- return generateToken(claims);
- }
- /**
- * 从令牌中获取用户名
- *
- * @param token 令牌
- * @return 用户名
- */
- public String getUsernameFromToken(String token) {
- String username;
- try {
- Claims claims = getClaimsFromToken(token);
- username = claims.getSubject();
- } catch (Exception e) {
- username = null;
- }
- return username;
- }
- /**
- * 判断令牌是否过期
- *
- * @param token 令牌
- * @return 是否过期
- */
- public Boolean isTokenExpired(String token) {
- try {
- Claims claims = getClaimsFromToken(token);
- Date expiration = claims.getExpiration();
- return expiration.before(new Date());
- } catch (Exception e) {
- return false;
- }
- }
- /**
- * 刷新令牌
- *
- * @param token 原令牌
- * @return 新令牌
- */
- public String refreshToken(String token) {
- String refreshedToken;
- try {
- Claims claims = getClaimsFromToken(token);
- claims.put("created", new Date());
- refreshedToken = generateToken(claims);
- } catch (Exception e) {
- refreshedToken = null;
- }
- return refreshedToken;
- }
- /**
- * 验证令牌
- *
- * @param token 令牌
- * @param userDetails 用户
- * @return 是否有效
- */
- public Boolean validateToken(String token, UserDetails userDetails) {
- SysUser user = (SysUser) userDetails;
- String username = getUsernameFromToken(token);
- return (username.equals(user.getUsername()) && !isTokenExpired(token));
- }
- /**
- * 从 claims 生成令牌, 如果看不懂就看谁调用它
- *
- * @param claims 数据声明
- * @return 令牌
- */
- private String generateToken(Map<String, Object> claims) {
- Date expirationDate = new Date(System.currentTimeMillis() + expiration);
- return Jwts.builder().setClaims(claims)
- .setExpiration(expirationDate)
- .signWith(SignatureAlgorithm.HS512, secret)
- .compact();
- }
- /**
- * 从令牌中获取数据声明, 如果看不懂就看谁调用它
- *
- * @param token 令牌
- * @return 数据声明
- */
- private Claims getClaimsFromToken(String token) {
- Claims claims;
- try {
- claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
- } catch (Exception e) {
- claims = null;
- }
- return claims;
- }
- }
上面的代码就是使用 io.jsonwebtoken.jjwt 提供的方法开发 JWT 令牌生成, 刷新的工具类.
三, 开发登录接口(获取 Token 的接口)
"/authentication" 接口用于登录验证, 并且生成 JWT 返回给客户端
"/refreshtoken" 接口用于刷新 JWT, 更新 JWT 令牌的有效期
- @RestController
- public class JwtAuthController {
- @Resource
- private JwtAuthService jwtAuthService;
- @PostMapping(value = "/authentication")
- public AjaxResponse login(@RequestBody Map<String, String> map) {
- String username = map.get("username");
- String password = map.get("password");
- if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
- return AjaxResponse.error(
- new CustomException(CustomExceptionType.USER_INPUT_ERROR,"用户名密码不能为空"));
- }
- return AjaxResponse.success(jwtAuthService.login(username, password));
- }
- @PostMapping(value = "/refreshtoken")
- public AjaxResponse refresh(@RequestHeader("${jwt.header}") String token) {
- return AjaxResponse.success(jwtAuthService.refreshToken(token));
- }
- }
核心的 token 业务逻辑写在 JwtAuthService 中
login 方法中首先使用用户名, 密码进行登录验证. 如果验证失败抛出 BadCredentialsException 异常. 如果验证成功, 程序继续向下走, 生成 JWT 响应给前端
refreshToken 方法只有在 JWT token 没有过期的情况下才能刷新, 过期了就不能刷新了. 需要重新登录.
- @Service
- public class JwtAuthService {
- @Resource
- private AuthenticationManager authenticationManager;
- @Resource
- private UserDetailsService userDetailsService;
- @Resource
- private JwtTokenUtil jwtTokenUtil;
- public String login(String username, String password) {
- // 使用用户名密码进行登录验证
- UsernamePasswordAuthenticationToken upToken =
- new UsernamePasswordAuthenticationToken( username, password );
- Authentication authentication = authenticationManager.authenticate(upToken);
- SecurityContextHolder.getContext().setAuthentication(authentication);
- // 生成 JWT
- UserDetails userDetails = userDetailsService.loadUserByUsername( username );
- return jwtTokenUtil.generateToken(userDetails);
- }
- public String refreshToken(String oldToken) {
- if (!jwtTokenUtil.isTokenExpired(oldToken)) {
- return jwtTokenUtil.refreshToken(oldToken);
- }
- return null;
- }
- }
因为使用到了 AuthenticationManager , 所以在继承 WebSecurityConfigurerAdapter 的 SpringSecurity 配置实现类中, 将 AuthenticationManager 声明为一个 Bean. 并将 "/authentication" 和 "/refreshtoken" 开放访问权限, 如何开放访问权限, 我们之前的文章已经讲过了.
- @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
- @Override
- public AuthenticationManager authenticationManagerBean() throws Exception {
- return super.authenticationManagerBean();
- }
四, 接口访问鉴权过滤器
当用户第一次登陆之后, 我们将 JWT 令牌返回给了客户端, 客户端应该将该令牌保存起来. 在进行接口请求的时候, 将令牌带上, 放到 HTTP 的 header 里面, header 的名字要和 jwt.header 的配置一致, 这样服务端才能解析到. 下面我们定义一个拦截器:
拦截接口请求, 从请求 request 获取 token, 从 token 中解析得到用户名
然后通过 UserDetailsService 获得系统用户(从数据库, 或其他其存储介质)
根据用户信息和 JWT 令牌, 验证系统用户与用户输入的一致性, 并判断 JWT 是否过期. 如果没有过期, 至此表明了该用户的确是该系统的用户.
但是, 你是系统用户不代表你可以访问所有的接口. 所以需要构造 UsernamePasswordAuthenticationToken 传递用户, 权限信息, 并将这些信息通过 authentication 告知 Spring Security.Spring Security 会以此判断你的接口访问权限.
- @Slf4j
- @Component
- public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
- @Resource
- private MyUserDetailsService userDetailsService;
- @Resource
- private JwtTokenUtil jwtTokenUtil;
- @Override
- protected void doFilterInternal(HttpServletRequest request,
- HttpServletResponse response,
- FilterChain chain) throws ServletException, IOException {
- // 从这里开始获取 request 中的 jwt token
- String authHeader = request.getHeader(jwtTokenUtil.getHeader());
- log.info("authHeader:{}", authHeader);
- // 验证 token 是否存在
- if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
- // 根据 token 获取用户名
- String username = jwtTokenUtil.getUsernameFromToken(authHeader);
- if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
- // 通过用户名 获取用户的信息
- UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
- // 验证 JWT 是否过期
- if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
- // 加载用户, 角色, 权限信息, Spring Security 根据这些信息判断接口的访问权限
- UsernamePasswordAuthenticationToken authentication
- = new UsernamePasswordAuthenticationToken(userDetails, null,
- userDetails.getAuthorities());
- authentication.setDetails(new WebAuthenticationDetailsSource()
- .buildDetails(request));
- SecurityContextHolder.getContext().setAuthentication(authentication);
- }
- }
- }
- chain.doFilter(request, response);
- }
- }
在 spring Security 的配置类 (即 WebSecurityConfigurerAdapter 实现类的 configure(HttpSecurity http) 配置方法中, 加入如下配置:
- .sessionManagement()
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
因为我们使用了 JWT, 表明了我们的应用是一个前后端分离的应用, 所以我们可以开启 STATELESS 禁止使用 session. 当然这并不绝对, 前后端分离的应用通过一些办法也是可以使用 session 的, 这不是本文的核心内容不做赘述.
将我们的自定义 jwtAuthenticationTokenFilter, 加载到 UsernamePasswordAuthenticationFilter 的前面.
五, 测试一下:
测试登录接口, 即: 获取 token 的接口. 输入正确的用户名, 密码即可获取 token.
下面我们访问一个我们定义的简单的接口 "/hello", 但是不传递 JWT 令牌, 结果是禁止访问. 当我们将上一步返回的 token, 传递到 header 中, 就能正常响应 hello 的接口结果.
期待您的关注
向您推荐博主的系列文档:《手摸手教您学习 SpringBoot 系列 - 16 章 97 节》 http://springboot.zimug.com/
来源: https://www.cnblogs.com/zimug/p/11974554.html