基于session认证上添加jwt认证
前言
此文章是基于已有SpringSecurity的默认cookie认证的基础上再新增jwt认证,这种适用于既可以运转传统的一体网站,又可以给只有前端的网站提供后台RestAPI服务。当然,此文章也可做为Spring Security兼容jwt的案例来看。
添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
对Spring Security的SecurityConfig的相关配置
我们已经有一个SecurityConfig配置文件用来配置默认cookie认证的相关信息,再新增一个JWTSecurityConfig配置文件,用@Order(1)标注(有标注的比没有此标注的优先匹配):
-
禁用Session(configure(HttpSecurity http)方法中);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
-
关闭csrf和frameOptions,如果不关闭会影响前端请求接口;
http.csrf().disable(); http.headers().frameOptions().disable();
-
开启跨域以便前端调用接口;
http.cors();
-
配置jwt认证作用下的所有接口,假如用“/jwt/”开头的接口都为jwt所有,"/jwt/login"是jwt下login的url;
http.antMatcher("/jwt/**") .authorizeRequests() // login url .antMatchers("/jwt/login").permitAll() // 其它所有接口需要认证才能访问 .anyRequest().authenticated() // 指定认证错误处理器 .and().exceptionHandling().authenticationEntryPoint(new MyEntryPoint());
-
在userpasswordFilter之前添加loginTokenFilter
http.addFilterBefore(loginTokenFilter, UsernamePasswordAuthenticationFilter.class);
login返回jwt过程
//LoginController
@RestController
@RequestMapping(value = "/jwt")
public class LoginController {
@Autowired
private GetTokenUserService getTokenUserService;
private static LogConfig log = LogConfig.getLogger(LoginController .class);
@RequestMapping(value = "/login", method = RequestMethod.POST)
public UserTokenBean systemLoginAction(@RequestBody LoginParam user) {
return getTokenUserService.login(user);
}
}
//GetTokenUserService
@Service
public class GetTokenUserService {
private static LogConfig log = LogConfig.getLogger(GetTokenUserService.class);
@Autowired
private JwtTokenUtil jwtTokenUtil;
public UserTokenBean login(LoginParam param) throws UsernameNotFoundException {
//省略查询条件
...
UserDetail userDetail = UserMapper.selectByExample(example);
UserTokenBean userTokenBean = new UserTokenBean();
userTokenBean.setUserDetail(userDetail);
// 生成JWT,将用户名数据存入其中
userTokenBean.setToken(jwtTokenUtil.generate(param.getLoginId()));
return userTokenBean;
}
}
//JwtTokenUtil
@Component
public class JwtTokenUtil {
/**
* 这个秘钥是防止JWT被篡改的关键,随便写什么都好,但决不能泄露
*/
private final static String secretKey = "whatever11gdb";
/**
* 过期时间目前设置成2天,这个配置随业务需求而定
*/
private final static Duration expiration = Duration.ofHours(2);
/**
* 生成JWT
*
* @param userName 用户名
* @return JWT
*/
public static String generate(String userName) {
// 过期时间
Date expiryDate = new Date(System.currentTimeMillis() + expiration.toMillis());
return Jwts.builder()
.setSubject(userName) // 将userName放进JWT
.setIssuedAt(new Date()) // 设置JWT签发时间
.setExpiration(expiryDate) // 设置过期时间
.signWith(SignatureAlgorithm.HS512, secretKey) // 设置加密算法和秘钥
.compact();
}
/**
* 解析JWT
*
* @param token JWT字符串
* @return 解析成功返回Claims对象,解析失败返回null
*/
public static Claims parse(String token) {
// 如果是空字符串直接返回null
if (StringUtils.isEmpty(token)) {
return null;
}
// 这个Claims对象包含了许多属性,比如签发时间、过期时间以及存放的数据等
Claims claims = null;
// 解析失败了会抛出异常,所以我们要捕捉一下。token过期、token非法都会导致解析失败
try {
claims = Jwts.parser()
.setSigningKey(secretKey) // 设置秘钥
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
// 这里应该用日志输出,为了演示方便就直接打印了
System.err.println("解析失败!");
}
return claims;
}
}
loginTokenFilter
@Component
public class LoginTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtManager;
@Autowired
private GetUserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 从请求头中获取token字符串并解析
Claims claims = jwtManager.parse(request.getHeader("Authorization"));
if (claims != null) {
// 从`JWT`中提取出之前存储好的用户名
String username = claims.getSubject();
// 查询出用户对象
UserDetails user = userService.loadUserByUsername(username);
// 手动组装一个认证对象
Authentication authentication = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
// 将认证对象放到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
自定义错误处理MyEntryPoint
@Component
public class MyEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
// 直接提示前端认证错误
out.write("认证错误");
out.flush();
out.close();
}
}