springSecurity + jwt实现token验证

jwts

jwt简介

  • JSON Web Token (JWT)是一种基于 token 的认证方案。

  • JWT就是一种Token的编码算法,服务器端负责根据一个密码和算法生成Token,然后发给客户端,客户端只负责后面每次请求都在HTTP header里面带上这个Token,服务器负责验证这个Token是不是合法的,有没有过期等,并可以解析出subject和claim里面的数据。

jwt组成

  • Header: 标题包含了令牌的元数据,并且在最小包含签名和/或加密算法的类型
  • Claims: Claims包含您想要签署的任何信息
  • JSON Web Signature (JWS): 在header中指定的使用该算法的数字签名和声明

jjwt

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

//下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body
.setClaims(claims)          //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId(jwtId)                  //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(now)           //iat: jwt的签发时间
.signWith(signatureAlgorithm, secretKey);//设置签名使用的签名算法和签名使用的秘钥

认证流程

集中式认证流程

  • 用户认证: 使用UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法实现认证功能,该过滤器父类中successfulAuthentication方法实现认证成功后的操作。
  • 身份校验: 使用BasicAuthenticationFilter过滤器中doFilterInternal方法验证是否登录,以决定能否进入后续过滤器。

分布式认证流程

  • 用户认证:由于分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步post的认证请求参数,需要修改UsernamePasswordAuthenticationFilter过滤器中attemptAuthentication方法,让其能够接收请求体。另外,默认successfulAuthentication方法在认证通过后,是把用户信息直接放入session就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。
  • 身份校验: 原来BasicAuthenticationFilter过滤器中doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息,我们要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便于后续的授权功能可以正常使用。

实例

添加依赖

<!--spring security 安全框架-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT工具依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

配置spring security

Spring Security 的核心配置就是继承 WebSecurityConfigurerAdapter 并注解@EnableWebSecurity 的配置。这个配置指明了用户名密码的处理方式、请求路径、登录、登出控制等和安全相关的配置。

package com.oyc.security.config;
 
import com.oyc.security.filter.TokenAuthenticationFilter;
import com.oyc.security.filter.TokenLoginFilter;
import com.oyc.security.handler.UnauthorizedEntryPoint;
import com.oyc.security.util.DefaultPasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 密码管理工具类
     */
    @Autowired
    private DefaultPasswordEncoder defaultPasswordEncoder;
 
    /**
     * 用户服务类
     */
    @Autowired
    private UserDetailsService userDetailsService;
 
    /**
     * 配置设置,设置退出的地址和token
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                //未授权处理
                .authenticationEntryPoint(new UnauthorizedEntryPoint())
                .and().authorizeRequests()
                .anyRequest().authenticated()
                .and().csrf().disable()
                .logout().logoutUrl("/logout")
                .and()
                //.addLogoutHandler(new TokenLogoutHandler(tokenManager))
                .addFilter(new TokenLoginFilter(authenticationManager()))
                .addFilter(new TokenAuthenticationFilter(authenticationManager())).httpBasic();
    }
 
    /**
     * 密码处理
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }
 
    /**
     * 配置哪些请求不拦截
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/index**", "/api/**", "/swagger-ui.html/**");
    }
}

认证授权相关工具类

密码处理类

package com.oyc.security.util;
 
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class DefaultPasswordEncoder extends BCryptPasswordEncoder {
 
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }
 
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        }
        if (encodedPassword == null || encodedPassword.length() == 0) {
            log.error("Empty encoded password");
            throw new IllegalArgumentException("encodedPassword is null");
        }
        return encodedPassword.equals(rawPassword);
    }
}

token操作类

package com.oyc.security.util;
 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
 
import java.util.Date;
 
public class JwtTokenUtil {
    private static long tokenExpiration = 24 * 60 * 60 * 1000;
    private static String tokenSignKey = "123456";
    private static String userRoleKey = "userRole";
 
    public String createToken(String userName) {
        String token = Jwts.builder().setSubject(userName)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        return token;
    }
 
    public static String createToken(String userName, String role) {
        String token = Jwts.builder().setSubject(userName)
                .claim(userRoleKey, role)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        return token;
    }
 
    public static String getUserNameFromToken(String token) {
        String userName = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return userName;
    }
 
    public static String getUserRoleFromToken(String token) {
        Claims claims = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody();
        return claims.get(userRoleKey).toString();
    }
 
}

接口响应类

package com.oyc.security.util;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
 
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
 

public class ResponseUtil {
    public static void out(HttpServletResponse response, Result result) {
        ObjectMapper mapper = new ObjectMapper();
        PrintWriter writer = null;
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            writer = response.getWriter();
            mapper.writeValue(writer, result);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                writer.flush();
                writer.close();
            }
        }
    }
}

认证授权filter

认证filter

此过滤器继承了 UsernamePasswordAuthenticationFilter 类,并重写了三个方法:

attemptAuthentication: 此方法用于验证用户登录信息;

successfulAuthentication: 此方法在用户验证成功后会调用;

unsuccessfulAuthentication: 此方法在用户验证失败后会调用。

package indi.xzw.license_server.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import indi.xzw.license_server.entity.RespBean;
import indi.xzw.license_server.util.JwtTokenUtil;
import indi.xzw.license_server.util.ResponseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.core.userdetails.User;

@Slf4j
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;

    public TokenLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        this.setPostOnly(false);
        // 指定需要验证的登录 URL
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
        try {
            // 获取用户登录信息
            String username = req.getParameter("username");
            String password = req.getParameter("password");
            username = username != null ? username.trim() : "";
            password = password != null ? password : "";
            // 根据用户名和密码生成身份验证信息
            Authentication authentication =new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
            return authenticationManager.authenticate(authentication);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 如果验证通过,就生成 token 并返回
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication auth) throws IOException, ServletException {
        //获取用户信息,获取身份信息
        User user = (User) auth.getPrincipal();
        String authrorities = user.getAuthorities().size() > 0 ? user.getAuthorities().toString().replaceAll("(?:\\[|null|\\]| +)", "") : user.getAuthorities().toString();

        // 生成 token
        String token = JwtTokenUtil.createToken(user.getUsername(), authrorities);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("token", token);
        //map.put("user", user);
        //map.put("loginName", user.getUsername());
        ResponseUtil.out(response, RespBean.ok(map));
        log.error("登录成功");
    }

    /**
     * 如果验证证不成功,返回错误信息提示
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, RespBean.error("登录失败"));
        log.error("登录失败");
    }
}

授权filter

所有的用户请求都会经过此过滤器,当请求进入过滤器后会经历如下步骤:

  • 首先,从请求中获取 token 信息,并检查 token 的有效性。

  • 如果 token 有效,则解析 token 获取用户名,然后使用用户名从数据库中获取用户角色信息,并在 Spring Security 的上下文中设置身份验证。

package indi.xzw.license_server.filter;

import indi.xzw.license_server.entity.RespBean;
import indi.xzw.license_server.util.JwtTokenUtil;
import indi.xzw.license_server.util.ResponseUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

/*
* JwtAuthorizationFilter 用户请求授权过滤器
 * 提供请求授权功能。用于处理所有 HTTP 请求,并检查是否存在带有正确 token 的 Authorization 标头。
 * 如果 token 有效,则过滤器会将身份验证数据添加到 Spring 的安全上下文中,并授权此次请求访问资源。</p>
* */
public class TokenAuthenticationFilter  extends BasicAuthenticationFilter {
    public TokenAuthenticationFilter(AuthenticationManager authManager) {
        super(authManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("=================" + request.getRequestURI());
        //不需要鉴权
        //if (request.getRequestURI().indexOf("index") != -1) {
        //    chain.doFilter(request, response);
       // }
        UsernamePasswordAuthenticationToken authentication = null;
        try {
            authentication = getAuthentication(request);
        } catch (Exception e) {
            ResponseUtil.out(response, RespBean.error(e.getMessage()));
        }
        if (authentication != null) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            ResponseUtil.out(response, RespBean.error("鉴权失败"));
        }
        //放行请求
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // 获取Token字符串,token 置于 header 里
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            token = request.getParameter("token");
        }
        if (token != null && !"".equals(token.trim())) {
            // 从Token中解密获取用户名
            String userName = JwtTokenUtil.getUserNameFromToken(token);

            if (userName != null) {
                // 从Token中解密获取用户角色
                String role = JwtTokenUtil.getUserRoleFromToken(token);
                // 将ROLE_XXX,ROLE_YYY格式的角色字符串转换为数组
                String[] roles = role.split(",");
                Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                for (String s : roles) {
                    authorities.add(new SimpleGrantedAuthority(s));
                }
                return new UsernamePasswordAuthenticationToken(userName, token, authorities);
            }
            return null;
        }
        return null;
    }
}

扩展

静态资源被拦截问题

重写WebSecurityConfigurerAdapter的configure方法

@Override
public void configure(WebSecurity web) {
//解决静态资源被拦截的问题
        		web.ignoring().antMatchers("/static/**","/css/**","/fonts/**","/img/**","/js/**","/favicon.ico","/error","/index.html");
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×