Spring Security + JWT实现登录
Spring Security + JWT实现登录
一、实现思路
登录
① 自定义登录接口 —> 通过调用ProviderManager验证是否登录成功 —> 成功后存Redis
② 自定义实现UserDetailService接口,在这个实现类中查询数据库
校验
定义JWT认证过滤器,解析token,获取其中的userId,从Redis中获取用户信息,存入SecurityContextHolder中
二、代码实现
① 流程:
a. 实现UserDetailService接口,重写loadUserByUserName()方法,从数据库获取用户的账号信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
// 如果没有查询到用户,就抛出异常
if (ObjectUtils.isEmpty(user)) {
throw new UsernameNotFoundException("账号或密码错误!");
}
// todo 查询对应的权限
// 把数据封装并返回
LoginUser loginUser = new LoginUser();
loginUser.setUser(user);
return loginUser;
}
}
b. 新建一个Security的配置类SecurityConfiguration,继承WebSecurityConfigurationadAdapter,在配置类中创及哦按方法passwordEncoder()方法,返回一个加密方式的对象,通常是BCryptPasswordEncoder。并将方法注册进Spring容器,用于替换默认的加密方式。
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* 创建BCryptPasswordEncoder注入到容器
* 使用 BCryptPasswordEncoder 替换 默认的PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
c. 在配置类中重写authenticationManager()方法,直接调用其父类方法,但是要将此方法注册到Spring容器中,方便service中调用。实际上这一步的目的就是将其注册到Spring容器中。
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 创建BCryptPasswordEncoder注入到容器
* 使用 BCryptPasswordEncoder 替换 默认的PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份验证
*/
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
d. 创建登录LoginController类和LoginService类,并分别常见登陆接口和login()方法,在login()方法中,首先利用AuthenticationManager的authenticate()方法进行身份认证,在认证通过后,生产JWT,并将用户信息封装到loginUser后存入Redis中。
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public String login(User user) {
// AuthenticationManager 的authenticate()方法进行身份认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证没通过
if (ObjectUtils.isEmpty(authenticate)) {
throw new RuntimeException("账号或密码错误!");
}
// 验证通过, 获取到loginUser, 使用userId 生产一个jwt返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String jwt = JwtUtil.createJWT(loginUser.getUser().getId().toString());
// 将用户信息存入Redis
redisCache.setCacheObject("loginUser-" + loginUser.getUser().getId(), JSON.toJSONString(loginUser.getUser()));
return jwt;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
/**
* 获取权限信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 获取密码
*/
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
e. 新建一个拦截器,拦截所有的web请求,检查请求中是否携带token,如果带了token,就进行一些列操作后放行,没带就直接放行。一些列操作包括:i. 获取携带的token信息;ii. 解析token得到userId(解析不出来就抛错:token非法!);iii. 根据解析的userId到Redis中获取用户信息,封装进loginUser(如果用户信息不存在,抛错:用户未登录!)。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
public RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("token");
// 如果携带了token, 就做一系列操作, 否则直接放行
if (StringUtils.hasText(token)) {
// 解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token 非法!");
}
// 从Redis中获取用户信息
User user = JSON.parseObject(JSON.toJSONString(redisCache.getCacheObject("loginUser-" + userId)), User.class);
if (ObjectUtils.isEmpty(user)) {
throw new RemoteException("用户未登陆!");
}
LoginUser loginUser = new LoginUser();
loginUser.setUser(user);
// 存入SecurityContextHolder, 因为后面的filter都是从SecurityContextHolder获取的
// todo 获取权限列表, 封装到authenticationToken
List authoritis = null;
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authoritis);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 放行
filterChain.doFilter(request, response);
}
}
f. 在Security配置类中,重写configure()方法,利用参数HttpSecurity控制请求的访问,且要将刚刚自定义的拦截器也插入到拦截器链上,加在身份认证之前。.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 创建BCryptPasswordEncoder注入到容器
* 使用 BCryptPasswordEncoder 替换 默认的PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份验证
*/
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* 拦截全部请求, 根据条件放行
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//把token校验过滤器添加到过滤器链中, 且在UsernamePasswordAuthenticationFilter之前
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
// 对于登录接口 允许匿名访问(匿名访问是 不携带token可以访问,携带不能访问)
.antMatchers("/user/login").anonymous()
// 任何人都能访问
.antMatchers("/index").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
;
}
}
② 完整demo
https://img.lyy52.wang/uPic/2022-05-22/spring-security.zip
参考资料
B站 https://www.bilibili.com/video/BV1mm4y1X7Hc?spm_id_from=333.337.search-card.all.click