SpringSecurity 无状态JWT验证

引言

朋友问到JWT的东西,其实这个东西很简单,只是将信息加密,以后前端访问带着这段信息过来,我们只需要解密后匹配一次看看信息是否正确即可。

必须使用JWT吗?
JWT只是一种标准,事实上你可以使用任何格式进行加密,没有必须让你写成json后加密。你可以将信息按照自己的格式进行序列化,然后进行加密。

(注意:JWT标准中的加密算法KEY只用来验签,防止数据篡改,其中payload只是明文base64编码,如果封装的payload有敏感数据,建议加一层rsa+aes的加密payload。标准中没有规定payload不能加密,你只需要在解析JWT的方法中增加对应的加密算法即可。)

使用Session和JWT的区别在于Session是服务器存了数据,默认是存在磁盘文件,也可以存在任何你想存的地方,比如redis,memcache,然后服务器会返回给你一段token当做cookie,这个token事实上就是存储的文件名(redis中是key),当你带着cookie来访问的时候,服务端就会在存储的地方通过这段cookie找到存储的信息。分布式中使用session你可以将信息存储到中间件中,比如redis,但是需要确保cookie的domain是一级域名,这种方式只能同域名或子域名实现共享。

后续有空会写OAuth2协议,还有吐槽一下好多人在gateway网关里转发jwt的操作,感觉是吃饱了撑的,gateway转发本身就可以带着header转发,为什么要再写一层filter???

pom.xml

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--JWT库-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.2</version>
        </dependency>

Security Config

主要是在UsernamePasswordAuthenticationFilter过滤器之前添加一个自定义过滤器

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

	//配置AuthenticationManager
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

	//用户信息服务 可以无视,如果测试可以直接用memory方式
	//这里只是继承了UserDetailsService,重写方法使用数据库读数据而已
    @Autowired
    private MyUserDetailsService myUserDetailsService;

	//自定义过滤器用来解析JWT信息,下面会写
    @Autowired
    private JWTAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", corsConfiguration);
		//配置访问权限 关闭csrf 允许跨域请求
        http.authorizeRequests().mvcMatchers("/login**").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable()
                .cors().configurationSource(source).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
        //在Security里UsernamePasswordAuthenticationFilter之前,添加一个过滤器(自定义的)
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(encoder());
        /*
		auth.inMemoryAuthentication()
		.withUser("test").password(bCryptPasswordEncoder().encode("test123"))
		.authorities("admin")
        .and().passwordEncoder(bCryptPasswordEncoder());
		*/
    }
}

编写JWTUtils 用来创建和解析JWT

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JWTUtils {

    public final Algorithm SIGN = Algorithm.HMAC256("123456789");

    public String createToken(UserDetails user){
        Map<String,String> claims= new HashMap<>();
        claims.put("username", user.getUsername());
        //JWT默认情况下HMAC只负责验签
        //其payload只是明文经过Base64编码
        //但我们的password是经过BCryptPasswordEncoder加密的
        //可以包含在payload中,我们仅仅只做碰撞判断密码是否被修改,让JWT失效而已。
        //更安全的做法是数据库中增加一个用来判断修改密码后的版本号,用版本号判断。
        claims.put("password", user.getPassword());
        return JWT.create()
                .withExpiresAt(new Date(System.currentTimeMillis() + 15 * 60 * 1000))  //设置过期时间
                .withClaim("principal",claims)
                .sign(SIGN);
    }


    public Map<String, Object> parseToken(String token){
        try{
            DecodedJWT verify = JWT.require(SIGN).build().verify(token);
            if (verify.getExpiresAt().after(new Date())){
                return verify.getClaim("principal").asMap();
            }
        }catch (JWTVerificationException ignored){
            return null;
        }
        return null;
    }

}

控制器 写login方法,返回JWT

@Controller
public class HomeController {
    @Autowired
    private JWTUtils jwt;

    @Autowired
    private AuthenticationManager authenticationManager;


    @RequestMapping("/login")
    @ResponseBody
    public String login(@RequestParam(value = "username") String username,@RequestParam(value = "password") String password) throws Exception {
        try {
        	//这里使用authenticationManager验证,最终还会用到Config中设置的userDetailsService的loadUserByUsername方法
        	//也可以直接用userDetailsService进行验证,反正只是为了封装JWT信息
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
            authenticationManager.authenticate(token);
        }catch (BadCredentialsException e){
            throw new Exception("账号密码错误",e);
        }
        return jwt.createToken(User.builder().username(username).password(password).authorities("Login").build());
    }

    @RequestMapping("/hello")
    @ResponseBody
    @PreAuthorize("hasAuthority('admin')")
    public String hello(){
        return "hello";
    }
}

JWTAuthenticationFilter 自定义过滤器

@Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JWTUtils jwt;
    @Autowired
    private MyUserDetailsService userDetailService;
    @Autowired
    private BCryptPasswordEncoder encoder;


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
		//获取Http头中的Authentication
        String authentication = httpServletRequest.getHeader("Authentication");
        if (!StringUtils.isNullOrEmpty(authentication)){
        	//解析JWT
            Map<String, Object> claims = jwt.parseToken(authentication);
           
            //如果解析后不为空,并且Security上下文中没有获取到验证信息(说明没有登录过,因为是无状态的,所以不会保存验证信息。)
            if (claims!=null && SecurityContextHolder.getContext().getAuthentication()==null){
                String username = (String) claims.get("username");
                String password = (String) claims.get("password");
                
                //重新经过userDetailService验证一次用户名和密码,因为JWT验证无法确保用户是否修改了密码,有被盗风险。
                UserDetails userDetails = userDetailService.loadUserByUsername(username);
                if (userDetails!=null){
                    if (encoder.matches(password,userDetails.getPassword())){
                        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, userDetails.getPassword(), userDetails.getAuthorities());
                        //token.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
						
						//在上下文中写入jwt解析后的信息,经过UsernamePasswordAuthenticationFilter时就会认为验证通过                        
                        SecurityContextHolder.getContext().setAuthentication(token);
                    }
                }
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}


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