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版权协议,转载请附上原文出处链接和本声明。