一、初步理解
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。
核心过滤器:
- (认证)UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和 AuthenticationException
- (授权)FilterSecurityInterceptor:负责权限校验的过滤器
二、Token(Jwt)登录校验流程
三、具体认证授权细节
下图是UsernamePasswordAuthenticationFilter处理用户名、密码,然后将用户名、密码、权限信息封装到Authentication对象中,再放到SecurityContextHolder中。
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的 方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装 成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
认证
- 当用户登录时,前端将用户输入的用户名、密码信息传输到后台,后台用一个类对象将其封装起来,通常使用的是UsernamePasswordAuthenticationToken这个类。
- 程序负责验证这个类对象。验证方法是调用Service根据username从数据库中取用户信息到实体类的实例中,比较两者的密码,如果密码正确就成功登陆,同时把包含着用户的用户名、密码、所具有的权限等信息(用户id、昵称、是否管理员)的类对象放到SecurityContextHolder(安全上下文容器,类似Session)中去。
- 用户访问一个资源的时候,首先判断是否是受限资源。如果是的话还要判断当前是否未登录,没有的话就跳到登录页面。
- 如果用户已经登录,访问一个受限资源的时候,程序要根据url去数据库中取出该资源所对应的所有可以访问的角色,然后拿着当前用户的所有角色一一对比,判断用户是否可以访问(这里就是和权限相关)。
授权
- 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
- 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。
自定义登录认证接口:①调用ProviderManager的方法进行认证;②如果认证通过生成jwt;③把用户信息存入redis中
自定义权限信息查询:在UserDetailsService这个实现类中去查询数据库
四、自定义权限查询
修改UsernamePasswordAuthenticationFilter上图最右边的授权部分。
1.自定义登陆接口
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public R login(@RequestBody User user) {
String jwt = userService.login(user);
if (StringUtils.hasLength(jwt)) {
return R.ok().message("登陆成功").data("token", jwt);
}
return R.error().message("登陆失败");
}
}
2.配置数据库校验登录用户
从之前的分析我们可以知道,我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的 UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
创建一个类实现UserDetailsService接口,重写loadUserByUsername方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.eq("user_name",username);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户,就抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//TODO 查询用户对应的权限信息
细节见SpringSecurity(二)——授权实现
//如果有,把数据封装成UserDetails对象返回
return new LoginUser(user);
}
}
五、Jwt认证过滤器(自定义过滤器)
(1)在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在 SecurityConfig中配置把AuthenticationManager注入容器。
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
/**
* 登录时需要调用AuthenticationManager.authenticate执行一次校验
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
(2)登录的业务逻辑层实现类
第一次登录,生成jwt存入redis
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public String login(User user) {
//1.封装Authentication对象 ,密码校验,自动完成
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
//2.进行校验
Authentication authenticate = authenticationManager.authenticate(authentication);
//3.如果authenticate为空
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败"); //TODO 登录失败
}
//4.得到用户信息
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
//生成jwt,使用fastjson的方法,把对象转成字符串
String loginUserString = JSON.toJSONString(loginUser);
//调用JWT工具类,生成jwt令牌
String jwt = JwtUtils.createJWT(loginUserString, null);
//5.把生成的jwt存到redis
String tokenKey = "token_" + jwt;
stringRedisTemplate.opsForValue().set(tokenKey, jwt, JwtUtils.JWT_TTL / 1000);
Map<String, Object> map = new HashMap<>();
map.put("token", jwt);
map.put("username", loginUser.getUsername());
return jwt;
}
}
(3)jwt认证校验过滤器
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的 userid。 使用userid去redis中获取对应的LoginUser对象。
然后封装Authentication对象存入SecurityContextHolder
/**
* token验证过滤器 //每一个servlet请求,只会执行一次
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
//1.获取当前请求的url地址
String url = request.getRequestURI();
//如果当前请求不是登录请求,则需要进行token验证
if (!url.equals("/user/login")) {
//2.验证token
this.validateToken(request);
}
} catch (AuthenticationException e) {
System.out.println(e);
loginFailureHandler.onAuthenticationFailure(request, response, e);
}
//3.登录请求不需要验证token
doFilter(request, response, filterChain);
}
/**
* 验证token
*/
private void validateToken(HttpServletRequest request) throws AuthenticationException {
//1.获取token
String token = request.getHeader("Authorization");
//如果请求头部没有获取到token,则从请求的参数中进行获取
if (ObjectUtils.isEmpty(token)) {
token = request.getParameter("Authorization");
}
if (ObjectUtils.isEmpty(token)) {
throw new CustomerAuthenticationException("token不存在");
}
//2.redis进行校验
String redisStr = stringRedisTemplate.opsForValue().get("token_" + token);
if(ObjectUtils.isEmpty(redisStr)) {
throw new CustomerAuthenticationException("token已过期");
}
//3.解析token
Claims claims = null;
try {
claims = JwtUtils.parseJWT(token);
} catch (Exception e) {
throw new CustomerAuthenticationException("token解析失败");
}
//4.获取到用户信息
String loginUserString = claims.getSubject();
//把字符串转成loginUser对象
LoginUser loginUser = JSON.parseObject(loginUserString, LoginUser.class);
//创建身份验证对象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//5.设置到Spring Security上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
(4)把jwt过滤器注册到springsecurity过滤器链中
放在UsernamePasswordAuthenticationFilter前面
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
//自定义jwt校验过滤器
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//配置关闭csrf机制
http.csrf(csrf -> csrf.disable());
//登陆失败处理器
http.formLogin(configurer -> {
configurer.failureHandler(loginFailureHandler);
});
http.sessionManagement(configurer ->
// STATELESS(无状态): 表示应用程序是无状态的,不会创建会话。
configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
//请求拦截方式
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login").permitAll()
.anyRequest().authenticated()
);
//!!!!!注册jwt过滤器!!!!!!!
http.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class);
//异常处理器
http.exceptionHandling(configurer -> {
configurer.accessDeniedHandler(customerAccessDeniedHandler);
configurer.authenticationEntryPoint(anonymousAuthenticationHandler);
});
return http.build(); //允许跨域
}
/**
* 登录时需要调用AuthenticationManager.authenticate执行一次校验
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}