Spring Security(7) jwt整合

jwt

基本介绍

jwt 全称是jsonWebToken, 简单的说就是一种能够携带信息的token。
在传统的web环境中,浏览器和后端通过记录在浏览器的cookie 和存储在服务端的session 来实现登录状态,而cookie session的方式在多分布式环境下可能带来session复制,跨域访问,单点登录等问题;
直接使用后端生成token的方式,服务端也需要存储生成的token信息,因为token是无意义的。而使用jwt ,能够携带一些必要得信息比如用户id 和用户名称等;
后端就不需要对生成的token做存储,同时jwt也有时间的有效期。能够做到请求接口无状态;

缺点:

  1. 安全性,payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。
  2. 无法废弃,只能等待过期失效,或增加其他的黑名单类似的逻辑处理失效。

jwt 官网: https://jwt.io/

格式

在使用过程中是一个base64编码的字符串

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

此字符串通过逗号分割是由3部分组成

第一部分是 header 区域,只要表示当前签名的加密方式;
第二部分是 plaoyload 区域,存储了当前的token携带的信息,包含颁发给谁,有效期等
第三部分是 将前2部分通过加密生成的,主要用于服务端校验token的合法性;

使用

基本依赖

引用对应的依赖,关于jwt的工具类有很多,这里使用 https://github.com/jwtk/jjwt

引入maven依赖

<!--api->
 <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
	</dependency>
<!---实现-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>


创建token使用

		//设置自定义header 信息
        JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("a", "b");

        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.MINUTE,30);

        Date expire = instance.getTime();

        //设置playload信息
        jwtBuilder = jwtBuilder.setIssuer("me")  //谁颁发的
                .setSubject("Bob")    // token的主体是什么 ,是关于什么的
                .setAudience("you")   // 给谁的
                .setExpiration(expire) //失效时间
                .setNotBefore(new Date()) //不能在此时间之前获取
                .setIssuedAt(new Date()) //签发时间
                .setId(UUID.randomUUID().toString());//id

        //设置自定义的playload信息
        jwtBuilder.claim("key","value");

        //构建签名算法,更多签名算法查看 SignatureAlgorithm
        Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        jwtBuilder = jwtBuilder.signWith(key);

          //执行压缩 使生成的字符串变小
        jwtBuilder = jwtBuilder.compressWith(CompressionCodecs.DEFLATE);

        System.out.println(jwtBuilder.compact());


解析读取token

        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder();
        //设置解析的签名算法
        jwtParserBuilder = jwtParserBuilder.setSigningKey(key);

        Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwtStr);

        String signature = claimsJws.getSignature();
        System.out.println("<========>");
        System.out.println(signature);
        JwsHeader header = claimsJws.getHeader();
        System.out.println(header);
        Claims body = claimsJws.getBody();
        System.out.println(body);


jackjson 的支持

引入依赖


<!--jackson 支持-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>


//设置序列化方式
jwtBuilder = jwtBuilder.serializeToJsonWith(new JacksonSerializer());

//设置反序列化方式
jwtParserBuilder = jwtParserBuilder.deserializeJsonWith(new JacksonDeserializer());

springsecurity 整合jwt

首先回忆一下springsecurity 的默认登录和鉴权流程;

springsecurity 中主要由一整套过滤器链来处理,不同的过滤器处理不同的功能;

  • ChannelProcessingFilter,因为它可能需要重定向到不同的协议
  • SecurityContextPersistenceFilter,因此可以在web请求开头的SecurityContextHolder中设置SecurityContext,并且SecurityContext的任何更改都可以复制到HttpSession当web请求结束时(准备好与下一个web请求一起使用)
  • ConcurrentSessionFilter,因为它使用SecurityContextHolder功能并需要更新SessionRegistry以反映来自校长的持续请求
  • 身份验证处理机制 - UsernamePasswordAuthenticationFilterCasAuthenticationFilterBasicAuthenticationFilter等 - 以便SecurityContextHolder可以修改为包含有效的Authentication请求令牌
  • SecurityContextHolderAwareRequestFilter,如果您使用它将Spring Security识别HttpServletRequestWrapper安装到您的servlet容器中
  • JaasApiIntegrationFilter,如果SecurityContextHolder位于SecurityContextHolder,则会将FilterChain视为JaasAuthenticationToken中的Subject
  • RememberMeAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,并且请求提供了一个启用记住我服务的cookie,则会在那里放置一个合适的记忆Authentication对象
  • AnonymousAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,那么匿名Authentication对象将被放置在那里
  • ExceptionTranslationFilter,捕获任何Spring Security异常,以便可以返回HTTP错误响应或启动适当的AuthenticationEntryPoint
  • FilterSecurityInterceptor,用于保护web URI并在访问被拒绝时引发异常

对于一个具有session登录的流程的过滤器链执行顺序是;

当发起登录请求的时候:

UsernamePasswordAuthenticationFilter 处理登录请求的参数和处理 将登录的信息放入session中;同时将Authentication 放入SecurityContext 中;

再次发起请求时 由 SecurityContextPersistenceFilter 从请求中获取请求的session信息获取Authentication 用于后续的流程校验;

整合基本要做的有

  1. 禁用session,不需要做任何session相关的处理。
  2. 未登录的时候能返回一个json提示,而不是登录页面。
  3. 登录成功后以json的返回成功和失败信息和jwt字符串
  4. 后续请求在header中卸载jwt 需要通过一个自定义的filter 从header中解析出来任何设置到 SecurityContext 中;

jwt 工具类

    <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> 
            <version>0.11.2</version>
            <scope>compile</scope>
        </dependency>
//提供了2个方法都是从UserDetails 中生成信息和获取信息
public class JwtUtil {

    private static Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    private static String ISS_USER = "web";

    private static String SUBJECT = "auth";



    /**
     * 创建jwt
     * @param userDetails
     * @return
     */
    public static String createJwt(UserDetails userDetails){
        JwtBuilder jwtBuilder = Jwts.builder();

        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.MINUTE,30);
        Date expire = instance.getTime();

        jwtBuilder = jwtBuilder.setIssuer(ISS_USER)
                .setSubject(SUBJECT)
                .setAudience(userDetails.getUsername())
                .setExpiration(expire)
                .setNotBefore(new Date())
                .setIssuedAt(new Date())
                .setId(userDetails.getUsername());

        JSONObject jsonObject = new JSONObject();
        jsonObject.putOpt("roleCodes",((MyUserDetails)userDetails).getRoleCodes());
        jsonObject.putOpt("permissionCodes",((MyUserDetails)userDetails).getPermissionCodes());
        jwtBuilder.addClaims(jsonObject);

        jwtBuilder = jwtBuilder.signWith(key);
        //执行压缩 使生成的字符串变小
        jwtBuilder = jwtBuilder.compressWith(CompressionCodecs.DEFLATE);
        jwtBuilder = jwtBuilder.serializeToJsonWith(new JacksonSerializer());
        return jwtBuilder.compact();
    }


    /**
     * 解析jwt
     * @param jwtStr
     * @return
     */
    public static UserDetails parseJwt(String jwtStr){
        JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder();
        jwtParserBuilder = jwtParserBuilder.setSigningKey(key);
        jwtParserBuilder = jwtParserBuilder.deserializeJsonWith(new JacksonDeserializer());
        Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwtStr);

        MyUserDetails userDetails = new MyUserDetails();
        Claims claims = claimsJws.getBody();
        userDetails.setUserName(claims.getId());
        userDetails.setRoleCodes((List)claims.get("roleCodes"));
        userDetails.setPermissionCodes((List)claims.get("permissionCodes"));
        return userDetails;
    }

}

配置无权限的json返回

 @Bean
    public AuthenticationEntryPoint authenticationEntryPoint(){
        return new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                JSONObject jsonObject = new JSONObject();
                jsonObject.putOpt("code","4001");
                jsonObject.putOpt("message","未登录");
                writeJson(response,jsonObject);
            }
        };
    }
    
     private void writeJson(HttpServletResponse response, JSONObject jsonObject){
        try {
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(jsonObject.toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    
    
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated().and().
        //配置exceptionHandling
        exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())

处理登录成功或失败的处理

在failureHandler 中处理登录错误的信息;

successHandler 中返回登录成功和jwt字符串信息;

sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 禁用session

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated().and().
        exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
                .and()
                .formLogin().loginProcessingUrl("/loginDo").failureHandler(new AuthenticationFailureHandler(){
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                        JSONObject jsonObject = new JSONObject();
                        jsonObject.putOpt("code","4002");
                        jsonObject.putOpt("message","登录错误");
                        writeJson(response,jsonObject);
                    }
                }).successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        JSONObject jsonObject = new JSONObject();
                        jsonObject.putOpt("code","2000");
                        jsonObject.putOpt("message","登录成功");
                        //生成token
                        Object getPrincipalObj = authentication.getPrincipal();
                        if(getPrincipalObj instanceof MyUserDetails){
                            MyUserDetails details = (MyUserDetails)getPrincipalObj;
                            String jwt = JwtUtil.createJwt(details);
                            jsonObject.putOpt("token",jwt);
                        }
                        writeJson(response,jsonObject);
                    }
                }).permitAll().and()
                .csrf().disable()
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().addFilterBefore(new TokenAuthFilter(authenticationManager(),authenticationEntryPoint()),UsernamePasswordAuthenticationFilter.class).httpBasic();
    }


添加 自定义解析jwtFilter

配置添加自定义的filter ,添加到 UsernamePasswordAuthenticationFilter 之前;

此filter主要逻辑是从header中解析 jwt 信息,如果能获取到就封装成UsernamePasswordAuthenticationToken 并设置到SecurityContext 中去.

public class TokenAuthFilter extends BasicAuthenticationFilter  {


    private AuthenticationEntryPoint authenticationEntryPoint;


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

    public TokenAuthFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
        super(authenticationManager, authenticationEntryPoint);
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            UsernamePasswordAuthenticationToken authRequest = getUsernamePasswordAuthenticationToken(request);
            if (authRequest == null) {
                this.logger.trace("Did not process authentication request since failed to find "
                        + "username and password in Basic Authorization header");
                chain.doFilter(request, response);
                return;
            }
            String username = authRequest.getName();
            this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
            if (authenticationIsRequired(username)) {
                SecurityContextHolder.getContext().setAuthentication(authRequest);
            }
        } catch (AuthenticationException ex) {
            SecurityContextHolder.clearContext();
            this.logger.debug("Failed to process authentication request", ex);
            onUnsuccessfulAuthentication(request, response, ex);
            this.authenticationEntryPoint.commence(request, response, ex);
            return;
        }
        chain.doFilter(request, response);
    }



    private boolean authenticationIsRequired(String username) {
       
        Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
        if (existingAuth == null || !existingAuth.isAuthenticated()) {
            return true;
        }
       
        if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) {
            return true;
        }
      
        return (existingAuth instanceof AnonymousAuthenticationToken);
    }




    private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(HttpServletRequest request) {
        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (header == null) {
            return null;
        }
        header = header.trim();
        try{
            UserDetails userDetails = JwtUtil.parseJwt(header);
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails.getUsername(),
                    userDetails.getPassword(),userDetails.getAuthorities());
            return token;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

测试

未携带jwt 或jwt错误返回

{
    "code": "4001",
    "message": "未登录"
}

执行登录返回jwt信息;


版权声明:本文为qq_34279995原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。