zHan_springsecurity笔记

138 阅读6分钟

引入依赖

    <dependencies>
        <!--springsecurity依赖-->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

    </dependencies>

完整流程

image-20230701110642377.png

UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor:负责权限校验的过滤器。

认证流程

ee29840985ea4802b01df57324e61924.png

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

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

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

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

实现步骤

登录

1177c85d5a384b41bc6840d3880f47dc.png

  1. 定义UserDetailsServiceImpl,实现UserDetailsService接口,返回UserDetails
  • 在这个实例中去查询数据库

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            // 查询用户信息
            LambdaQueryWrapper<User> queryWrapper=new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getUserName,username);
            User user = userMapper.selectOne(queryWrapper);
    
            // 如果没有查询到用户,抛出异常
            if (Objects.isNull(user)){
                throw new RuntimeException("用户不存在");
            }
    
            // TODO 查询权限信息
    
            // 返回封装的UserDetails对象
            return new LoginUserDetails(user);
        }
    }
    
  • 创建UserDetails

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class LoginUserDetails implements UserDetails {
    
        private User user;
    
        // TODO 获取权限信息
        @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;
        }
    }
    
    
  • 定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new MD5PasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
        http
        //关闭csrf
        .csrf().disable()
        // session管理
        .sessionManagement()
        //不通过Session获取SecurityContext
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        // 权限请求管理
        .authorizeRequests()
        // 对于登录接口 允许匿名访问
        .antMatchers("/user/login").anonymous()
        // 所有资源都可访问
        .antMatchers("/hello").permitAll()
        // 除上面外的所有请求全部需要鉴权认证
        .anyRequest().authenticated();
        }
    
      @Bean
      @Override
      public AuthenticationManager authenticationManagerBean() throws Exception {
          return super.authenticationManagerBean();
    
        }
    }
    
    
    
    
    
  • 创建MD5PasswordEncoder,实现PasswordEncoder,用于md5加密

    public class MD5PasswordEncoder implements PasswordEncoder {
        // 加密
        @Override
        public String encode(CharSequence oldPassword) {
            return MD5Utils.encrypt(oldPassword.toString());
        }
    
        // 解密
        @Override
        public boolean matches(CharSequence oldPassword, String encodePassword) {
            return encodePassword.equals(encode(oldPassword));
        }
    }
    

2.自定义登录接口

调用ProviderManager的方法进行认证

  • 如果认证通过,生成JWT,把用户存入redis:userid作为key,用户信息作为value
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public Result login(User user) {
        // AuthenticationManager来用户认证
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authentication = authenticationManager.authenticate(authenticationToken);

        // 如果认证失败,给出对应提示
        if (Objects.isNull(authentication)) {
            throw new RuntimeException("用户名或密码错误,登录失败");
        }

        // 如果认证通过,使用userId生成一个jwt,存入result返回
        LoginUserDetails loginUserDetails = (LoginUserDetails) authentication.getPrincipal();
        String userId = String.valueOf(loginUserDetails.getUser().getId());
        String jwt = JwtUtils.createJWT(userId);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);

        // 把完整的用户信息存入redis,userId为key
        redisCache.setCacheObject("login:" + userId, loginUserDetails);

        return new Result(200, "登录成功", map);
    }
}

校验

image-20230701160150953.png

  1. 定义JWT认证过滤器(继承OncePerRequestFilter)
    • 获取token

    • 解析token获取其中的userid

    • 从redis中获取用户信息

    • 存入SecurityContextHolder

      @Component
      public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
      
          @Autowired
          private RedisCache redisCache;
      
          @Override
          protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
              // 获取token
              String token = request.getHeader("token");
              if (!StringUtils.hasText(token)) {
                  // 放行
                  filterChain.doFilter(request, response);
                  return;
              }
      
              // 解析token
              String userId;
              try {
                  Claims claims = JwtUtils.parseJWT(token);
                  userId = claims.getSubject();
              } catch (Exception e) {
                  e.printStackTrace();
                  throw new RuntimeException("token非法");
              }
      
              // 从redis中获取用户信息
              LoginUserDetails loginUserDetails = redisCache.getCacheObject("token" + userId);
              if (Objects.isNull(loginUserDetails)){
                  throw new RuntimeException("用户未登录");
              }
      
              // 存入SecurityContextHolder
              // TODO 获取权限消息封装到Authentication中
              UsernamePasswordAuthenticationToken authenticationToken=
                      new UsernamePasswordAuthenticationToken(loginUserDetails,
                              null,null);
              SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      
              // 放行
              filterChain.doFilter(request, response);
          }
      }
      
      • 过滤器配置,在SecurityConfig类的configure方法里添加

        http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
        

        注意:注入JwtAuthenticationTokenFilter

        @Autowired
        private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
        

退出登录

定义一个登录接口,获取SecurityContextHolder的认证信息,删除redis对应的记录即可

在LoginServiceImpl中

@Override
    public Result logout() {
        // 获取SecurityContextHolder的认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUserDetails loginUserDetails = (LoginUserDetails) authentication.getPrincipal();
        Long userId = loginUserDetails.getUser().getId();
        // 删除redis对应的记录
        redisCache.deleteObject("login:" + userId);
        return new Result(200,"退出登录成功");
    }

授权流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

  1. 设置我们的资源所需要的权限即可。

    • 在SecurityConfig类上添加
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    
    • 在想要添加权限的controller中的方法上添加注解

      @PreAuthorize("hasAuthority('system:dept:list')")
      
  2. 把当前登录用户的权限信息也存入Authentication。(完成前面的TODO)

    • 补全登录,在LoginUserDetails类上添加:

      //存储SpringSecurity所需要的权限信息的集合
          @JSONField(serialize = false)   // 表示不会序列化到redis中
          private List<GrantedAuthority> authorities;
      
          // TODO 获取权限信息
          @Override
          public Collection<? extends GrantedAuthority> getAuthorities() {
              if (authorities != null) {
                  return authorities;
              }
              // 把permissions的string类型的权限信息封装成SimpleGrantedAuthority
              authorities = permissions.stream()
                      .map(SimpleGrantedAuthority::new)
                      .collect(Collectors.toList());
              return authorities;
          }
      
    • 补全校验,在JwtAuthenticationTokenFilter过滤器类上添加:

      // TODO 获取权限消息封装到Authentication中
      UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(loginUserDetails,null,loginUserDetails.getAuthorities());
      
  3. 从数据库查找权限信息(PBAC权限模型)

    • MenuMapper类

      List<String> selectParamsByUserId(@Param("userId") Long userId);
      
    • MenuMapper.xml

          <select id="selectParamsByUserId" resultType="java.lang.String">
              SELECT distinct m.perms
              FROM sys_menu as m
                       INNER JOIN sys_role_menu as rm on m.id = rm.menu_id
                       INNER JOIN sys_user_role as ur on rm.role_id = ur.role_id
              WHERE ur.user_id = #{userId}
                AND m.`status` = 0
          </select>
      
      • 注意:在application.xml完成配置:

        mybatis-plus:
          mapper-locations: classpath*:/mapper/**/*.xml
        
    • 在UserDetailsServiceImpl类上完成TODO

    // TODO 查询权限信息
    List<String> list = menuMapper.selectParamsByUserId(user.getId());
    
    // 返回封装的UserDetails对象
    return new LoginUserDetails(user,list);
    

自定义失败处理

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。

在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

认证过程中出现的异常:会被封装成AuthenticationException,然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

授权过程中出现的异常:会被封装成AccessDeniedException,然后调用AccessDeniedHandler对象的方法去进行异常处理。

Web工具类

public class WebUtils {
    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}

实现

认证:自定义AuthenticationEntryPoint,实现AuthenticationEntryPoint接口

授权:自定义AccessDeniedHandler,实现AccessDeniedHandler接口

自定义实现类

  1. 认证

    @Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException e) throws IOException, ServletException {
            Result result = new Result(HttpStatus.FORBIDDEN.value(), "认证识别,请查询用户");
            // 转为json格式
            String json = JSON.toJSONString(result);
            WebUtils.renderString(response, json);
        }
    }
    
  2. 授权

    @Component
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response,
                           AccessDeniedException e) throws IOException, ServletException {
            Result result = new Result(HttpStatus.FORBIDDEN.value(), "权限不足");
            // 转为json格式
            String json = JSON.toJSONString(result);
            WebUtils.renderString(response,json);
        }
    }
    

配置类上添加配置

在SecurityConfig类的configure方法里

// 配置自定义异常处理
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
    .accessDeniedHandler(accessDeniedHandler);

注意:要先注入对应的处理器

@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private AccessDeniedHandler accessDeniedHandler;

解决跨域问题

  1. 先对SpringBoot配置,运行跨域请求:CorsConfig类中

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
          // 设置允许跨域的路径
            registry.addMapping("/**")
                    // 设置允许跨域请求的域名
                    .allowedOriginPatterns("*")
                    // 是否允许cookie
                    .allowCredentials(true)
                    // 设置允许的请求方式
                    .allowedMethods("GET", "POST", "DELETE", "PUT")
                    // 设置允许的header属性
                    .allowedHeaders("*")
                    // 跨域允许时间
                    .maxAge(3600);
        }
    }
    
    
  2. 开启SpringSecurity的跨域访问:在SecurityConfig类的configure方法里

    // 开启跨域配置
    http.cors();
    

- 权限控制

权限校验方法

  • hasAuthority:用户符合的权限才可以访问对应资源

    @PreAuthorize("hasAuthority('system:dept:list')")
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
    
  • hasAnyAuthority:可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

    @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
    
  • hasRole:要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。

    @PreAuthorize("hasRole('system:dept:list')")
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
    
  • hasAnyRole:有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。

    @PreAuthorize("hasAnyRole('admin','system:dept:list')")
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
    

自定义权限校验方法

  1. 定义自己的权限校验方法,在@PreAuthorize注解中使用我们的方法。

    @Component("ex")
    public class SGExpressionRoot {
    
        public boolean hasAuthority(String authority){
            //获取当前用户的权限
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            List<String> permissions = loginUser.getPermissions();
            //判断用户权限集合中是否存在authority
            return permissions.contains(authority);
        }
    }
    
  2. 在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法

    @PreAuthorize("@ex.hasAuthority('system:dept:list')")
    @RequestMapping("/hello")
    public String hello(){
        return "hello dept";
    }
    

基于配置的权限控制

在SecurityConfig类的configure方法里

http.authorizeRequests()
    // 权限配置
    .antMatchers("/hello").hasAuthority("system:dept:list")