SpringSecurity 认证实现流程分析
目录
- 一、初步理解
- 二、Token(Jwt)登录校验流程
- 三、具体认证授权细节
- 四、自定义权限查询
- 1.自定义登陆接口
- 2.配置数据库校验登录用户
- 五、Jwt认证过滤器(自定义过滤器)
一、初步理解
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 {js @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.登录请求不需要javascript验证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不存在"); } adPSvnZWbe //2.redis进行校验 String redisStr = stringRedisTemplate.opsForValue().get("token_" + token); if(ObjectUtils.iphpsEmpty(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 UsernamePasswordAuthenticationTok编程en(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(); } }
到此这篇关于SpringSecurity 认证实现的文章就介绍到这了,更多相关SpringSecurity 认证内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论