SpringSecurity(一)——认证实现

一、初步理解

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

核心过滤器:

  • (认证)UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和 AuthenticationException
  • (授权)FilterSecurityInterceptor:负责权限校验的过滤器

二、Token(Jwt)登录校验流程

三、具体认证授权细节

下图是UsernamePasswordAuthenticationFilter处理用户名、密码,然后将用户名、密码、权限信息封装到Authentication对象中,再放到SecurityContextHolder中。

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的 方法。

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装 成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

认证

  1. 当用户登录时,前端将用户输入的用户名、密码信息传输到后台,后台用一个类对象将其封装起来,通常使用的是UsernamePasswordAuthenticationToken这个类。
  2. 程序负责验证这个类对象。验证方法是调用Service根据username从数据库中取用户信息到实体类的实例中,比较两者的密码,如果密码正确就成功登陆,同时把**包含着用户的用户名、密码、所具有的权限等信息(用户id、昵称、是否管理员)**的类对象放到SecurityContextHolder(安全上下文容器,类似Session)中去。
  3. 用户访问一个资源的时候,首先判断是否是受限资源。如果是的话还要判断当前是否未登录,没有的话就跳到登录页面。
  4. 如果用户已经登录,访问一个受限资源的时候,程序要根据url去数据库中取出该资源所对应的所有可以访问的角色,然后拿着当前用户的所有角色一一对比,判断用户是否可以访问(这里就是和权限相关)。

授权

  1. 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
  2. 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。

自定义登录认证接口:①调用ProviderManager的方法进行认证;②如果认证通过生成jwt;③把用户信息存入redis中

自定义权限信息查询:在UserDetailsService这个实现类中去查询数据库

四、自定义权限查询

修改UsernamePasswordAuthenticationFilter上图最右边的授权部分。

1.自定义登陆接口

java 复制代码
@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方法

java 复制代码
@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注入容器。

java 复制代码
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
    /**
     * 登录时需要调用AuthenticationManager.authenticate执行一次校验
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

(2)登录的业务逻辑层实现类

第一次登录,生成jwt存入redis

java 复制代码
@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

java 复制代码
/**
 * 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前面

java 复制代码
@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();
    }
}
相关推荐
Oliver_LaVine25 分钟前
linux搭建Gray
运维
gobeyye33 分钟前
Docker 用法详解
运维·docker·容器
VVVVWeiYee1 小时前
Mesh路由组网
运维·网络·智能路由器·信息与通信
IT枫斗者2 小时前
如何解决Java EasyExcel 导出报内存溢出
java·服务器·开发语言·网络·分布式·物联网
北'辰2 小时前
使用ENSP实现DHCP+动态路由
运维·网络
air_7292 小时前
实验四:构建园区网(OSPF 动态路由)
服务器·网络·智能路由器
雪碧聊技术2 小时前
Docker3:docker基础1
运维·docker·容器·docker常见命令
懒笑翻2 小时前
Python 使用 Selenuim进行自动化点击入门,谷歌驱动,以百度为例
运维·selenium·自动化
n***85942 小时前
Github 开源 10K Stars 自动化 API、后台作业、工作流和 UI 的开发平台
运维·自动化
夜色呦2 小时前
中小企业人事管理自动化:SpringBoot实践
运维·spring boot·自动化