Ooooon

灯光照亮着我

【spring security】JWT理解

session认证流程

1、用户提交认证信息到认证服务,认证通过后服务器创建session,返回sessionID

2、浏览器保存sessionID到cookie中

3、请求资源时携带cookie,服务器根据sessionID判断是否完成认证

缺点:

  • 占用服务器内存
  • 在分布式系统中,每台服务器都有自己独立的session,session资源无法共享
  • cookie安全问题等

token认证流程

1、客户端请求认证服务

2、服务器接收请求进行认证

3、认证成功后签发token给客户端

4、客户端保存token

5、客户端请求其他资源服务时携带token

6、服务器对token进行校验,校验成功之后可以访问资源

特点:

  • Token完全由应用管理,所以它可以避开同源策略
  • Token可以避免CSRF攻击
  • Token可以是无状态的,可以在多个服务间共享

详细介绍---------> https://www.cnblogs.com/qdhxhz/p/13337919.html

JWT

JWT,全称JSON Web Token。可以生成token,也可以解析检验token。

JWT生成token的结构

JWT生成的token有三部分组成,header(头信息)、Payload(载荷)、Signature(签名),中间使用“.”链接。

qqq.wwwwwwww.eeeeeeee
  • 头信息:主要设置一些规范信息,签名部分的编码格式以及令牌类型。
{
  "alg": "HS256",
  "typ": "JWT"
}
  • 载荷:token中存放有效信息的部分,比如用户名,用户角色,过期时间等,此部分可以随意读取所以不能放入敏感信息
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
  • 签名:将头部与载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。
 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);

(加盐:使用一个字符串与原字符串组成一个新字符串,再对新字符串加密。就是秘钥)

非对称加密RSA

上面的token,对于头信息和载荷来说只是进行了base64编码相当于是透明的。能够保证安全性的只有签名部分加入的盐,如果生成token的盐和解析token的盐是一样,那token就可以随便伪造了,所以就需要对盐采用非对称加密的方式进行加密,以达到生成token与校验token方所用的盐不一致的安全效果!

  • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
    • 私钥加密,持有私钥或公钥才可以解密
    • 公钥加密,持有私钥才可解密
  • 优点:安全,难以破解,载荷中可以有用户信息减少查库,json形式跨语言
  • 缺点:算法比较耗时

JWT认证授权实现

public class JwtTokenUtils {

    private static final String JWT_PAYLOAD_USER_KEY = "user";

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位分钟
     * @return JWT
     */
    public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey,
                                                      int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JSON.toJSONString(userInfo))
                .setId(createJTI())
                .setExpiration(DateUtils.asDate(LocalDateTime.now().plusMinutes(expire)))
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();

    }

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位秒
     * @return JWT
     */
    public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey,
                                                      int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JSON.toJSONString(userInfo))
                .setId(createJTI())
                .setExpiration(DateUtils.asDate(LocalDateTime.now().plusSeconds(expire)))
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥
     * @return Jws<Claims>
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    private static String createJTI() {
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T>
            userType) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserInfo(JSONObject.parseObject(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
        claims.setExpiration(body.getExpiration());
        return claims;
    }

    /**
     * 获取token中的载荷信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setExpiration(body.getExpiration());
        return claims;
    }

}
public class RsaUtils {

    private static final int DEFAULT_KEY_SIZE = 2048;

    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    private static PublicKey getPublicKey(byte[] bytes) throws Exception {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException,
            InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String
            secret, int keySize) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }

}


认证服务

在前面的代码里有写一个自定义的登录成功的处理器,我直接在这个处理器里生成token返回

/**
 * 用户 登录 成功---处理器
 */
@Slf4j
@Component
public class CustomSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @SneakyThrows
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登陆成功,用户名 = {},信息 = {}", authentication.getName(), JSONObject.toJSONString(authentication));
        UserDTO principal = (UserDTO)authentication.getPrincipal();

        Map<String, Object> userMap = new HashMap<>();
        userMap.put("username", authentication.getName());
        userMap.put("roleList", principal.getRoleList());

        String token = JwtTokenUtils.generateTokenExpireInMinutes(userMap, RsaUtils.getPrivateKey("E:\\key\\auth_key\\rsa_key"), 24 * 60);

        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(token);
        out.flush();
        out.close();
    }

}

可以在配置里禁用session

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ......
         // 禁用session
        http
                							.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        .....
    }

资源服务

public class TokenVerifyFilter extends BasicAuthenticationFilter {

    public TokenVerifyFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    /**
     * 过滤请求
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) {
        try {
            //请求体的头中是否包含Authorization
            String header = request.getHeader("Authorization");
            //Authorization中是否包含Bearer,不包含直接返回
            if (header == null || !header.startsWith("Bearer ")) {
                chain.doFilter(request, response);
                toResponse(response);
                return;
            }
            //获取权限失败,会抛出异常,这里直接用了UsernamePasswordAuthenticationToken,可以自己再实现一个token来用
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
            //获取后,将Authentication写入SecurityContextHolder中供后序使用
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } catch (Exception e) {
            toResponse(response);
            e.printStackTrace();
        }
    }

    /**
     * 未登录提示
     *
     * @param response
     */
    private void toResponse(HttpServletResponse response) {
        try {
            //未登录提示
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            PrintWriter out = response.getWriter();
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("code", HttpServletResponse.SC_FORBIDDEN);
            map.put("message", "请登录!");
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
    }

    /**
     * 通过token,获取用户信息
     *	
     * @param request
     * @return
     */
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) throws Exception {
        String token = request.getHeader("Authorization");
        if (token != null) {
            //通过token解析出载荷信息
            Payload<UserDTO> payload = JwtUtils.getInfoFromToken(token.replace("Bearer ", ""), RsaUtils.getPublicKey("E:\\key\\auth_key\\rsa_key.pub"), UserDTO.class);
            UserDTO user = payload.getUserInfo();
            //不为null,返回
            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            }
            return null;
        }
        return null;
    }
}

配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;

    /**
     * 配置静态资源
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    /**
     * 配置安全策略
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        TokenVerifyFilter tokenVerifyFilter = new TokenVerifyFilter(authenticationManagerBean());
        // 过滤器配置
        http
                .addFilter(tokenVerifyFilter);
        // 禁用session
        http
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        // 禁用csrf
        http.csrf().disable()
                // 请求配置
                .authorizeRequests()
                // 配置请求权限 hasRole  hasAuthority 两种方式差一个 ROLE_ 前缀
                .antMatchers("/sys/**").hasAuthority("ROLE_SYS")
                .antMatchers("/user/**").hasAuthority("ROLE_USER")
                .antMatchers("/car/**").hasAuthority("ROLE_CAR")
                // 其他接口都需要进行认证
                .anyRequest().authenticated()
                .and().exceptionHandling()
                // 认证失败的处理器
                .accessDeniedHandler(accessDeniedHandler);
    }

}

posted @ 2021-10-21 17:44  Ooooon  阅读(446)  评论(0)    收藏  举报