关于java:Spring-Security-JWT

18次阅读

共计 7378 个字符,预计需要花费 19 分钟才能阅读完成。

Spring Security 默认是基于 session 进行用户认证的,用户通过登录申请实现认证之后,认证信息在服务器端保留在 session 中,之后的申请发送上来后 SecurityContextPersistenceFilter 过滤器从 session 中获取认证信息、以便通过后续平安过滤器的安全检查。

明天的指标是替换 Spring Security 默认的 session 保留认证信息的机制为 通过 JWT 的形式进行认证。

JWT(JSON WEB TOKEN)的相干内容就不做详细分析了,咱们只须要晓得以下几点:

  1. 用户登录认证(用户名、明码验证)通过之后,系统生成 token 并送给前端。
  2. token 中蕴含用户 id(或用户名)以及过期工夫,蕴含通过加密机制生成的摘要,具备防篡改的能力。
  3. token 信息不须要在服务器端保留,前端获取到 token 之后,每次申请都必须携带该 token。
  4. 后盾接管到申请之后,查看没有 token、或者 token 验证不通过则不生成认证信息,否则,token 验证通过则示意该用户通过认证。
  5. 后盾接管到的 token 如果已过期,则依据利用的需要自动更新 token 或者要求前端从新登录。

与 session 计划比照一下,咱们须要解决的问题如下:

  1. 须要停用掉 Spring Security 默认的 session 治理用户认证信息的计划。
  2. 用户登录后须要生成并返回给前端 token。
  3. 前端申请上来之后,须要获取并验证 token,验证通过后生成用户认证信息。

上面咱们逐个解决上述三个问题。咱们依然应用上一篇文章中用过的 demo,曾经贴出过的代码就不再贴出了。

筹备工作

咱们须要筹备一些与 JWT 相干的货色,比方引入 JWT 的生成 token、token 验证的模块。

咱们引入 java-jwt, 在 pom 文件退出依赖即可:

<dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.2.1</version>
        </dependency>

而后须要编写一个工具类,以便可能生成、验证 token,咱们临时不思考 token 过期等等细节问题的解决,只有能正确生成、验证 token 就能够:

public class JwtUtil {
    public final static String SECRET_KEY="This is secret key for JWT";
    public final static String JWTHeader_Leading_Str="Bearer";
    public final static String JWTHeader_Name="Authorization";

    public static String generateToken(String userName){Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND,120);
        HashMap header = new HashMap<>();
        header.put("alg","HS256");
        header.put("Type","JWT");
        return JWT.create().withHeader(header)
                .withClaim("userName",userName)
                .withExpiresAt(calendar.getTime())
                .sign(Algorithm.HMAC256(SECRET_KEY));
    }

    public static String verify(String token){
        // 创立解析对象,应用的算法和 secret 要与创立 token 时保持一致
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
        // 解析指定的 token
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        return decodedJWT.getClaims().get("userName").asString();}

    public static String parseToken(HttpServletRequest request){String rawJwt = request.getHeader(JWTHeader_Name);
        if(rawJwt==null){return null;}
        if(!rawJwt.startsWith(JWTHeader_Leading_Str)){return null;}
        return rawJwt.substring(JWTHeader_Leading_Str.length()+1);
    }

    private void showToken(DecodedJWT decodedJWT){
        // 获取解析后的 token 中的信息
        String header = decodedJWT.getHeader();
        System.out.println("type:" + decodedJWT.getType());
        System.out.println("header:" + header);
        Map<String, Claim> payloadMap = decodedJWT.getClaims();
        System.out.println("Payload:" + payloadMap);
        Date expires = decodedJWT.getExpiresAt();
        System.out.println("过期工夫:" + expires);
        String signature = decodedJWT.getSignature();
        System.out.println("signature:" + signature);
    }


    public static void main(String[] args) {String token=JwtUtil.generateToken("Zhang Fu");
        System.out.println(token);
        String userName = JwtUtil.verify(token);
        System.out.println("userName:" +userName);
    }
}

OK,筹备工作实现。

停用 Spring Security 的默认 session 计划

为了停用 session,咱们须要减少一项配置,所以咱们要新建一个配置文件:

@Configuration
public class WebSecurityConfig {

    @Autowired
    MyRememberMeService myRememberMeService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception
    {httpSecurity.authorizeRequests()
                //.antMatchers("/hello").permitAll()
                .anyRequest().authenticated().and()
                .httpBasic().and()
                .rememberMe().rememberMeServices(myRememberMeService)
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .formLogin();
        //httpSecurity.addFilterBefore(new JwtSecurityFilter(), UsernamePasswordAuthenticationFilter.class);
        //httpSecurity.addFilterAfter(new JwtAfterUsernamePasswordFilter(),UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();}
}

设置 SessionCreationPolicy.STATELESS 就能够达到目标。

起因能够在 sessionManagementConfigure.java 这个 session 配置器中找到,在他的 init 办法中:

@Override
    public void init(H http) {SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
        boolean stateless = isStateless();
        if (securityContextRepository == null) {if (stateless) {http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());
            }

如果 SessionCreationPolicy 设置为 stateless 的话,那么他会创立 NullSecurityContextRepository 作为他的 SecurityContextRepository。

这个 NullSecurityContextRepository 理论就是个假把式,啥也不干,咱们晓得用户认证通过后会调用他的 saveContext 办法存储认证信息,他是这么干的:

@Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {}

所以,他就是个偷工减料的货,啥也没干。

所以第一个问题解决了。

用户登录后生成 token 并返回给前端

这个问题我尝试了好几个计划之后才胜利。

咱们晓得用户登录是在平安过滤器 UsernamePasswordAuthenticationFilter 中实现的,登录胜利后如果想要生成 JWT 的 token,计划无非就是:

  1. UsernamePasswordAuthenticationFilter 之后加一个咱们本人的过滤器,与 UsernamePasswordAuthenticationFilter 一样只匹配登录申请,生成 token。
  2. UsernamePasswordAuthenticationFilter 过滤器认证通过后有没有调用过其余能够被咱们客户化的东东,咱们客户化这个东东实现咱们的指标。
  3. 客户化 UsernamePasswordAuthenticationFilter,登录胜利后生成 token。

这里必须交代一下,第 3 个计划只是从逻辑上来说应该能解决咱们的问题,然而压根就没有思考过这个计划,因为我感觉太麻烦。

先试了第一个计划,没胜利,因为咱们晓得 Spring Security 还有一个 RequestCacheAwareFilter 过滤器,会导致如果你是在尚未获取受权之前拜访了非登录页面,那么 Spring Security 会导航到登录页面、登录胜利后在 UsernamePasswordAuthenticationFilter 中就会产生跳转,这样的话就跳过了咱们前面加的这个过滤器,指标就无奈实现或者说即便弯弯绕绕能实现,然而计划也不会太好。

所以,就致力钻研第 2 个计划。

所以大略看了一下 UsernamePasswordAuthenticationFilter 在登录认证胜利后的解决,发现了这个:

所以就大略去钻研了一下 RememberMeServices,读了一下他的 doc,发现他是一个基于 cokie 的、确保前台申请即便在 session 过期之后发送上来都能够持续通过平安认证的“记住我”机制。

除了 cokie 之外,其余的与 JWT 的要求齐全吻合。如果咱们能本人实现一个基于 JWT 的 RememberMeService,是不是就解决问题了?

所以就用他来尝试一下。

MyRememberMeService#loginSuccess

创立 MyRememberMeService 并通过配置退出到利用中来,后面停用 session 的时候的配置文件中曾经退出了,返回去看一眼就行。

咱们要实现他的 loginSuccess 办法,创立并返回 token:

@Override
    public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
                             Authentication successfulAuthentication) {String username = successfulAuthentication.getName();
        String token= JwtUtil.generateToken(username);
        token=JwtUtil.JWTHeader_Leading_Str+token;
        log.info("After login success:"+token);
        response.setHeader(JwtUtil.JWTHeader_Name,token);
    }

验证一下创立并返回 token

启动我的项目,胜利登录零碎后,惊喜的发现他曾经开始干活了:

好了,给了咱们信念,撸起袖子加油干!

RememberMeAuthenticationFilter

RememberMeServices 机制依赖 RememberMeAuthenticationFilter 实现,咱们在下面的配置文件中曾经启用了。

而后简略看一眼 RememberMeAuthenticationFilter 过滤器的 doFilter 办法,他首先去 SecurityContextHolder 获取认证信息,如果没有获取到的话,就调用 RememberMeService 的 autoLogin 办法,只是从 doFilter 的源码来看(代码就不贴出了),autoLogin 办法返回的 Authentication 并未实现认证,因为返回之后还要调用 authenticationManager 进行认证。

这是与咱们预期不符的中央,咱们心愿 autoLogin 之后就能够实现认证、并且能够将认证信息搁置到 SecurityContextHolder 中(因为咱们是通过 JWT 做验证的,token 验证通过的话就相当于实现了认证)。

那咱们是不是能够在 autoLogin 中实现这些操作,并且返回 null 骗一下 RememberMeAuthenticationFilter 的 doFilter 办法不再要求 authenticationManager 去再次认证呢?

咱们试一下!

MyRememberMeService#autoLogin

创立 RememberMeService 并实现 autoLogin 办法,为了简化他的初始化过程,咱们间接把他注入到 Spring Ioc 容器中。

如前所述,办法肯定要返回 null。

@Slf4j
@Component
public class MyRememberMeService implements RememberMeServices {
    @Autowired
    MyUserDetailsService myUserDetailsService;
    @Override
    public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {log.info("autoLogin in MyRememberMeService:");
        String username;
        String token = JwtUtil.parseToken(request);
        if(token==null){log.info("I dont get token from header");
            token=request.getParameter("token");
        }
        log.info("finally the token is :" + token);
        UsernamePasswordAuthenticationToken authenticationToken=null;
        if(token!=null) {String userName=JwtUtil.verify(token);
            UserDetails user = myUserDetailsService.loadUserByUsername(userName);
            authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        return null;
    }

    @Override
    public void loginFail(HttpServletRequest request, HttpServletResponse response) {}}

测试一下 autoLogin

下面的代码中曾经看到了,咱们只是为了测试、如果从申请头信息中拿不到 token 的话就从申请参数中获取。只是为了学习、测试偷个懒,正式我的项目实现的时候这个中央还是须要比拟多的欠缺的。

启动我的项目,开始测试,第一步先通过 login 获取 token,下面曾经展现过了,而后用获取到的 token 发一个须要认证的申请,token 加在申请参数前面:

如图,申请胜利了!

上一篇 Spring Security 自定义用户认证过程(2)

正文完
 0