当我们使用SpringBoot实现了一个简单的API接口之后,我们如何去保证我们的API接口只让我们运行的人调用呢。这时候就需要对我们的API接口进行保护。在别人访问这些接口的时候,我们对访问者进行身份的验证,从而对接口的保护。

基本流程如下图:

java 鉴权验签 java 接口鉴权_spring boot

当然我们需要一个接口给用户请求Token,要不然用户拿不到token怎么去请求其他资源呢。流程图如下:

java 鉴权验签 java 接口鉴权_spring boot_02

接下来我们按照流程一步一步实现。

首先实现请求token

@GetMapping("/get_token")
    public JsonResult getToken(@RequestParam String username,@RequestParam String password){
        //apiUser通过用户url传入的账号和密码
        ApiUser apiUser=loginService.loginApiUser(username);
        //这里验证用户的信息是否正确
        JsonResult jsonResult=checkApiUser(apiUser,password);
        //如果正确那么返回Null,不正确返回提示然后显示给用户
        if (jsonResult!=null){
            return jsonResult;
        }
        //到这里证明用户信息是正确的,那么我们进行token的生成然后返回给用户
        String token=loginService.generateToken(apiUser);

        return JsonResult.suc(token);
    }
  1. 通过url拿到用户传过来的数据,账户和密码
  2. 对账户密码进行验证,如果不正确返回错误提示。
  3. 如果正确进行token生存,然后返回给用户

那么我们如何验证呢

private JsonResult checkApiUser(ApiUser apiUser,String password){
        if (apiUser==null){
            return JsonResult.error(434,"账户不存在");
        }else {
            if (apiUser.getEnable()==0){
                return JsonResult.error(452,"账户在黑名单");
            }
            if (!apiUser.getPassword().equals(password)){
                //equals相等返回true
                return JsonResult.error(452,"账户密码错误");

            }

        }
        return null;
    }
  1. 先通过用户传入的用户名进行查找,看是否存在该用户。
  2. 如果存在该用户,判断用户的状态是否可用。
  3. 如果状态可用,那么比较数据库保存的密码和用户输入的密码是否匹配。

那么如何生成token呢

public String generateToken(ApiUser tokenDetail) {
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("sub", tokenDetail.getUsername());
        claims.put("created", this.generateCurrentDate());
        return this.generateToken(claims);
    }

    /**
     * 根据 claims 生成 Token
     *
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        logger.info("成功进入生产token",claims);
        try {
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(this.generateExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, this.secret.getBytes("UTF-8"))
                    .compact();
        } catch (UnsupportedEncodingException ex) {
            //didn't want to have this method throw the exception, would rather log it and sign the token like it was before
            logger.warn(ex.getMessage());
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(this.generateExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, this.secret)
                    .compact();
        }
    }
  1. 我们通过JWT工具去进行token的生成。(如果不了解JWT可用百度看看,非常简单。)
  2. 通过用户信息,我们生成一个token并返回给客户端。
  3. 用户拿着这个token去请求资源,就可以了。

接下来我们看用户请求资源是如何实现的

@PostMapping("/courseList")
    public JsonResult getAllCourse(Page<Course> page, Integer state){
        //获取当前学年的课程列表

        return JsonResult.suc(courseService.getAll(page,state));
    }

这是一个资源API接口,那么用户在访问这个接口的时候我们就需要对身份进行验证,那么是在哪里验证的呢

@Configuration      // 声明为配置类
@EnableWebSecurity      // 启用 Spring Security web 安全的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 注册 401 处理器
     */
    @Autowired
    private EntryPointUnauthorizedHandler unauthorizedHandler;

    /**
     * 注册 403 处理器
     */
    @Autowired
    private MyAccessDeniedHandler accessDeniedHandler;

    /**
     * 注册 token 转换拦截器为 bean
     * 如果客户端传来了 token ,那么通过拦截器解析 token 赋予用户权限
     *
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        AuthenticationTokenFilter authenticationTokenFilter = new AuthenticationTokenFilter();
        authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
        return authenticationTokenFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/get_token").permitAll()     // 所有人可以访问
                .anyRequest().authenticated()       // 必须携带token
                .and()
                // 配置被拦截时的处理
                .exceptionHandling()
                .authenticationEntryPoint(this.unauthorizedHandler)   // 添加 token 无效或者没有携带 token 时的处理
                .accessDeniedHandler(this.accessDeniedHandler)      //添加无权限时的处理
                .and()
                .csrf()
                .disable()                      // 禁用 Spring Security 自带的跨域处理
                .sessionManagement()                        // 定制我们自己的 session 策略
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 调整为让 Spring Security 不创建和使用 session


        /**
         * 本次 json web token 权限控制的核心配置部分
         * 在 Spring Security 开始判断本次会话是否有权限时的前一瞬间
         * 通过添加过滤器将 token 解析,将用户所有的权限写入本次会话
         */
        http
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }
}
  1. 首先我们通过SpringSecurity进行url配置,也就是给URL加了过滤器。
  2. 当用户访问需要token的资源路径的时候就会触发过滤器。
  3. http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);这一句相当于把我们自定义的过滤器加入到Springsecurity过滤器链中。
@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 将 ServletRequest 转换为 HttpServletRequest 才能拿到请求头中的 token
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        // 尝试获取请求头的 token
        String authToken =httpRequest.getHeader(this.tokenHeader);//获取token=xxx

        // 尝试拿 token 中的 username
        // 若是没有 token 或者拿 username 时出现异常,那么 username 为 null
        String username = this.tokenUtils.getUsernameFromToken(authToken);

        // 如果上面解析 token 成功并且拿到了 username 并且本次会话的权限还未被写入
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 用 UserDetailsService 从数据库中拿到用户的 UserDetails 类
            // UserDetails 类是 Spring Security 用于保存用户权限的实体类
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            // 检查用户带来的 token 是否有效
            // 包括 token 和 userDetails 中用户名是否一样, token 是否过期, token 生成时间是否在最后一次密码修改时间之前
            // 若是检查通过
            if (this.tokenUtils.validateToken(authToken, userDetails)) {
                // 生成通过认证
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
                // 将权限写入本次会话
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
            if (!userDetails.isEnabled()){
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().print("{\"code\":\"452\",\"data\":\"\",\"message\":\"账号处于黑名单\"}");
                return;
            }
        }

        chain.doFilter(request, response);
    }
  1. 在自定义过滤器中通过 String authToken =httpRequest.getHeader(this.tokenHeader);这一句代码我们获得了用户传入的token值。
  2. 通过对token值的解析我们可以获取到用户信息,这里的信息在是在生成token的时候我们加入到token中的。(获取到用户信息你可以对用户操作进行一些记录。)同时如果获取的用户信息不存在我们数据库中,那么证明该token不是正确的,不在进行后面的业务逻辑
  3. 用户信息正确,我们对token进行验证看token是否过期,或者用户是否已经被禁用了。 this.tokenUtils.validateToken(authToken, userDetails)这一句进行验证

这样完整的鉴权流程我们就实现了。上面只是部分代码。

源码:https://github.com/xushuoAI/Springboot-SpringSecurity-Mybatis-Redis-

以上代码参考了许多博主的博文。非常感谢各位前辈的分享。