SpringSecurity代码实现JWT接口权限授予与校验

经过笔者前两篇文章的说明,相信你们已经知道JWT是什么,怎么用,该如何结合Spring Security使用。那么本节就用代码来具体的实现一下JWT登陆认证及鉴权的流程。前端

1、环境准备工做

  • 创建Spring Boot项目并集成了Spring Security,项目能够正常启动
  • 经过controller写一个HTTP的GET方法服务接口,好比:“/hello”
  • 实现最基本的动态数据验证及权限分配,即实现UserDetailsService接口和UserDetails接口。这两个接口都是向Spring Security提供用户、角色、权限等校验信息的接口
  • 若是你学习过Spring Security的formLogin登陆模式,请将HttpSecurity配置中的formLogin()配置段所有去掉。由于JWT彻底使用JSON接口,没有from表单提交。
  • HttpSecurity配置中必定要加上csrf().disable(),即暂时关掉跨站攻击CSRF的防护。这样是不安全的,咱们后续章节再作处理。

以上的内容,咱们在以前的文章中都已经讲过。若是仍然不熟悉,能够翻看本号以前的文章。web

2、开发JWT工具类

经过maven坐标引入JWT工具包jjwtspring

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>复制代码

在application.yml中加入以下自定义一些关于JWT的配置数据库

jwt: 
  header: JWTHeaderName
  secret: aabbccdd  
  expiration: 3600000   复制代码
  • 其中header是携带JWT令牌的HTTP的Header的名称。虽然我这里叫作JWTHeaderName,可是在实际生产中可读性越差越安全。
  • secret是用来为JWT基础信息加密和解密的密钥。虽然我在这里在配置文件写死了,可是在实际生产中一般不直接写在配置文件里面。而是经过应用的启动参数传递,而且须要按期修改。
  • expiration是JWT令牌的有效时间。

写一个Spring Boot配置自动加载的工具类。json

@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令牌生成、刷新的工具类。后端

3、开发登陆接口(获取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"开放访问权限,如何开放访问权限,咱们以前的文章已经讲过了。springboot

@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}复制代码

4、接口访问鉴权过滤器

当用户第一次登录以后,咱们将JWT令牌返回给了客户端,客户端应该将该令牌保存起来。在进行接口请求的时候,将令牌带上,放到HTTP的header里面,header的名字要和jwt.header的配置一致,这样服务端才能解析到。下面咱们定义一个拦截器:session

  • 拦截接口请求,从请求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)配置方法中,加入以下配置:app

.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);复制代码

  • 由于咱们使用了JWT,代表了咱们的应用是一个先后端分离的应用,因此咱们能够开启STATELESS禁止使用session。固然这并不绝对,先后端分离的应用经过一些办法也是可使用session的,这不是本文的核心内容不作赘述。
  • 将咱们的自定义jwtAuthenticationTokenFilter,加载到UsernamePasswordAuthenticationFilter的前面。

5、测试一下:

测试登陆接口,即:获取token的接口。输入正确的用户名、密码便可获取token。

file

下面咱们访问一个咱们定义的简单的接口“/hello”,可是不传递JWT令牌,结果是禁止访问。当咱们将上一步返回的token,传递到header中,就能正常响应hello的接口结果。

file

期待您的关注

相关文章
相关标签/搜索