spring security and jwt
写在前面
Q: 为什么是spring security
A: 无论是企业还是开源项目 一遍都会拿shiro作为首选,理由很简单
shiro简单 易于上手 文档教程多,随便参考一个开源项目都可以写出标准的业务代码。
但 spring security就不是这样了
根据我目前的使用来讲 你必须要了解spring security源代码 清楚执行流程,才能使用该框架。
并且 spring security可以不依赖servlet
Q:为什么需要JWT
A: 这里就会牵扯到基于token认证 和 基于传统的session认证
http本身是一种无状态的认证 这就意味着每一次请求 我们都不知道访问主体是谁
为了能够分辨访问主体 我们会在用户认证完毕之后给用户颁发一个标识 (sessionid 或者 token)
传统的session 会将session context存储到后端 将sessionid存储到cookie中 服务器收到请求后 取出cookie中的sessionid 根据sessionid 从session repository取出session context,如果取不到则认为是未认证请求。
而 jwt则是将context存储到token上 每次请求都要吧token放在header上 服务区收到请求后 解析header 取出token 只需要验证token是否有效 如果需要用户相关的信息 只需要从token中直接拿出来就可以了
这样 token也做到了无状态
do it
集成
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
不需要添加版本号 springboot中有对应的
下一步之前
在下一步之前 请确保项目跑的起来 并且建立起基本的配置 并且能够以默认配置进行登陆
本篇不会讲过多的基础内容 我只会讲 如何优雅地使用spring security
如有错误 欢迎指正
登陆
在使用默认的内存账户登陆之后 我们需要完成基于数据库的用户认证
我们需要的流程是
用户输入账号密码 -> spring security -> 根据账号密码匹配在数据库中的记录
如果成功将创建会话 否则将提示错误信息
那么好 我们先从登陆入口开始
spring security的登陆是依靠filter的 默认的路径是/login 浏览器打开则是spring security的默认登录页 如果用POST方法请求 则是正常的登陆逻辑
所以我们复用这个filter
那么载入用户的信息 UserDetailsService 则需要改造成从数据库读取就可以了
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userService.getUserByUsername(s);
if (user == null)
throw new UsernameNotFoundException("user not found");
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.emptyList());
}
}
ok 问题解决
那么此时 用户的密码是用明文保存的 如果数据库遭到泄露 用户信息安全则是一个大问题
一般比较安全的做法是 签名算法(密码 + 盐值)
但是spring security在user里面似乎没有提供盐值这个属性
不用慌
原来spring security早就准备好了一套解决方案 我们就不需要关心 盐值如何生成 如何存储
BCryptPasswordEncoder
生成的结果是这样样子的
{bcrypt}$2a$10$wGhC6bsgTghxFLFt1Aqbl.7t.MU/6mJLSkMwzTXxvRhZJToCWHQ5i
有加密算法 有盐值 有最终结果 all perfect
在 security config里这么做就可以了
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
有关PasswordEncoderFactories 请自行查阅资料 它是一个更强的PasswordEncoder
注册
我们搞定了登陆部分 但是我如何注册呢?
如何保存加密的密码呢?
其实最核心的代码就是
passwordEncoder.encode(password);
他的返回值就是加盐加密后的密码 我们只需要存储它就OK了
当然这个注册的controller还是要自己写的
jwt认证
注册 登陆 都没有问题了
我们发现 session还是传统session 这对于前后端分离的应用不是那么优雅
那么 我们可以先看看security是怎么实现session的(源代码需要自己啃)
非常关键的几个类
SessionManagementConfigurer
session的配置类
SessionManagementFilter
session的过滤器
SecurityContextRepository
控制session读取和保存
其实我们只需要根据这些定制出jwt的内容就可以了
首先 JwtAuthenticationToken 用于验证和以后使用的用途
@Getter
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private String token;
public JwtAuthenticationToken(String token, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.token = token;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return JwtUtil.getClaim(token, JwtUtil.JWT_USERNAME);
}
}
写出repository 用于提供jwt的读取和保存
@Slf4j
public class JwtSecurityContextRepository implements SecurityContextRepository {
private static JwtSecurityContextRepository securityContextRepository = new JwtSecurityContextRepository();
private JwtSecurityContextRepository() {
}
public static JwtSecurityContextRepository getInstance() {
return securityContextRepository;
}
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
String authorization = requestResponseHolder.getRequest().getHeader(LoginConst.AUTHORIZATION_HEADER_NAME);
if (StringUtils.isNotBlank(authorization) && TOKEN_SESSION.containsKey(authorization.substring(7))) {
SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(new JwtAuthenticationToken(authorization.substring(7), Collections.emptyList()));
return securityContext;
}
return generateNewContext();
}
protected SecurityContext generateNewContext() {
return SecurityContextHolder.createEmptyContext();
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
final Authentication authentication = context.getAuthentication();
if (authentication == null) {
//trustResolver.isAnonymous(authentication)
log.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
return;
}
//如果是jwt的token则不需要再次保存(签发)
if (!(authentication instanceof UsernamePasswordAuthenticationToken))
return;
User principal = (User)authentication.getPrincipal();
String code = UUIDUtil.generateShortUuid();
String token = JwtUtil.sign(principal.getUsername(), code);
//签发之后 缓存中保存一份 用于校验token和secret
TOKEN_SESSION.put(token, code);
response.setHeader(LoginConst.AUTHORIZATION_HEADER_NAME, LoginConst.AUTHORIZATION_PREFIX + token);
}
@Override
public boolean containsContext(HttpServletRequest request) {
String authorization = request.getHeader(LoginConst.AUTHORIZATION_HEADER_NAME);
return StringUtils.isNotBlank(authorization) && TOKEN_SESSION.containsKey(authorization.substring(7));
}
}
接着写Filter
@Slf4j
@Setter
public class JwtManagementFilter extends GenericFilter {
static final String FILTER_APPLIED = "__spring_security_session_jwt_filter_applied";
private SecurityContextRepository securityContextRepository;
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
private InvalidSessionStrategy invalidSessionStrategy = null;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getAttribute(FILTER_APPLIED) != null) {
filterChain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//校验 jwt
if (!securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null && !trustResolver.isAnonymous(authentication)) {
// The user has been authenticated during the current request, so call the
// session strategy
securityContextRepository.saveContext(SecurityContextHolder.getContext(),
request, response);
}
else {
// No security context or authentication present. Check for a session
// timeout
if (isAuthorizationValid(request)) {
if (invalidSessionStrategy != null) {
invalidSessionStrategy
.onInvalidSessionDetected(request, response);
return;
}
}
}
}
filterChain.doFilter(request, response);
}
private boolean isAuthorizationValid(HttpServletRequest request) {
String authorization = request.getHeader(LoginConst.AUTHORIZATION_HEADER_NAME);
return StringUtils.isNotBlank(authorization);
}
}
Configurer类 用来配置Filter和一些其他的组件
public class JwtManagementConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<JwtManagementConfigurer<H>, H> {
@Override
public void configure(H builder) throws Exception {
JwtManagementFilter jwtManagementFilter = new JwtManagementFilter();
jwtManagementFilter.setSecurityContextRepository(JwtSecurityContextRepository.getInstance());
JwtManagementFilter filter = postProcess(jwtManagementFilter);
builder.addFilterBefore(filter, SessionManagementFilter.class);
}
}
这个时候问题来了 拦截的逻辑都写好了 但是具体的认证逻辑是在哪里呢
就是 JwtAuthenticationProvider
public class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
JwtAuthenticationToken jwtAuthentication = (JwtAuthenticationToken) authentication;
String token = jwtAuthentication.getToken();
if (TOKEN_SESSION.containsKey(token))
return authentication;
throw new BadCredentialsException("token 已失效");
}
@Override
public boolean supports(Class<?> authentication) {
return JwtAuthenticationToken.class.isAssignableFrom(authentication);
}
}
最后 把这些配置放到配置文件中
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关掉csrf方便httpclient调试和以后的jwt token
.csrf().disable()
.formLogin()
.and()
//所有的请求都需要认证
.authorizeRequests()
.anyRequest()
.authenticated()
//无状态session
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//JWT相关配置
.and()
.apply(new JwtManagementConfigurer<>())
//JWT认证Provider
.and()
.authenticationProvider(new JwtAuthenticationProvider());
}
不要忘记了repository
不止我们的配置需要用 所以需要放到shareobject里一份
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.setSharedObject(SecurityContextRepository.class, JwtSecurityContextRepository.getInstance());
}
一个最简单的spring security就配置完成了
这里面虽然没有涉及到权限 也没有做严格的校验
仔细看看源代码 你就知道 如何拓展权限和完整的校验
项目地址 Github:shenlanAZ/accelerator
tag:spring-security