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,咱们须要减少一项配置,所以咱们要新建一个配置文件:

@Configurationpublic 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@Componentpublic 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)