登录
① 自定义登录接口 —> 通过调用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() ; } }
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