SpringSecurity权限控制的学习

发布时间 2023-11-11 15:38:20作者: 陈强强强强强

Security权限控制流程

 

环境

数据库

数据库采用RBAC结构,大概如下图所示

创建的表结构如下所示,用户表,角色表,权限表和两个关联他们的表

img

导入springsecurity依赖坐标和我们需要的各种依赖坐标

  <!--        mysql 的驱动和mybatisplus依赖-->

       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>8.0.29</version>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>3.3.1</version>
       </dependency>

       <!--       lombok-->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</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.83</version>
       </dependency>
       <!--jwt依赖-->
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt-api</artifactId>
           <version>0.11.2</version>
       </dependency>
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt-impl</artifactId>
           <version>0.11.2</version>
           <scope>runtime</scope>
       </dependency>
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
           <version>0.11.2</version>
           <scope>runtime</scope>
       </dependency>

   <!--       security-->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
   </dependency>

然后在pom中配置mysql配置,redis配置,mybatisplus配置

 

 

SpringSecurity异常捕获

因为security一般不会被我们自己定义的全局异常捕获而捕获。

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

所以我们这里代码这样子写

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
   @Override
   public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
       ResponseResult result = new ResponseResult(401, authException.getMessage());
       String json = JSON.toJSONString(result);
       WebUtils.renderString(response, json);
  }
}
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
   @Override
   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
       ResponseResult result = new ResponseResult(403, accessDeniedException.getMessage());
       String json = JSON.toJSONString(result);
       WebUtils.renderString(response, json);
  }
}

然后要在security的配置类中注册。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   //密码加密方式
   @Bean
   public PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }
   //过滤器
   @Resource
   JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
   //注入   身份认证和权限控制异常捕获
   @Autowired
   private AuthenticationEntryPoint authenticationEntryPoint;
   @Autowired
   private AccessDeniedHandler accessDeniedHandler;
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       // 从数据库读取的用户进行身份认证
       super.configure(auth);
  }
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http
               //关闭csrf
              .csrf().disable()
               //不通过Session获取SecurityContext
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
              .and()
              .authorizeRequests()
               // 对于登录接口 允许匿名访问 已经登录后不可访问
              .antMatchers("/user/login").anonymous()
//               .antMatchers("").permitAll()   无论是否认证对资源放行
               // 除上面外的所有请求全部需要鉴权认证
              .anyRequest().authenticated();
       //捕获security异常统一返回
       http.exceptionHandling()
              .authenticationEntryPoint(authenticationEntryPoint)
              .accessDeniedHandler(accessDeniedHandler);

       //把token校验过滤器添加到过滤器链中
       http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

  }

   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception {
       return super.authenticationManagerBean();
  }
}

登录代码

定义数据库中user对应的实体类和mapper还有service

在登录的service中写一个登录的方法

再写一个该方法的实现类在实现类中写入以下代码

    @Override
   public ResponseResult login(User user) {

           //是一个将账号密码封装起来的一个方法
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
           // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername   返回一个LoginUser里面包含账号密码和对应的权限
           Authentication authenticate = authenticationManager.authenticate(authenticationToken);
return null;

  }

因为authenticationManager.authenticate()会调用UserDetailsService所以我们写一个UserdetailsService的实现类并且重写里面的loadUserByUsername。

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   //判断用户表中是否有该用户,并且返回该用户的所有信息
   LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
   wrapper.eq(User::getUserName,username);
   User user = userMapper.selectOne(wrapper);
   
   return createLoginUser(user);
}

一般是在这个类中以用户的用户名查询用户信息,再接着去查询用户所拥有的权限然后给到springSecurity。因为这个方法要返回一个userDetails类型的数据,所以我们要接他的接口重写里面的方法,然后可以添加一些我们自己需要的数据类型。

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

   private User user;

   //存储权限信息
   private List<String> permissions;

   /**
    *
    * 用户的唯一标识
    */
   private String token;

   public String getToken() {
       return token;
  }

   public void setToken(String token) {
       this.token = token;
  }

   public LoginUser(User user, List<String> permissions) {
       this.user = user;
       this.permissions = permissions;
  }
   public LoginUser(User user) {
       this.user = user;

  }


   //存储SpringSecurity所需要的权限信息的集合
   @JSONField(serialize = false)
   private List<GrantedAuthority> authorities;

   @Override
   public  Collection<? extends GrantedAuthority> getAuthorities() {
       if(authorities!=null){
           return authorities;
      }
       //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
       authorities = permissions.stream().
               map(SimpleGrantedAuthority::new)
              .collect(Collectors.toList());
       return authorities;
  }

//   获取密码
   @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;
  }
}

然后我们继续写上面的createLogin()方法

public  LoginUser createLoginUser(User user){
   //在这里创建返回需要的loginUser并且查询用户的所有权限
   return new LoginUser(user,permissionService.getRolePermision(user));
}

这边我们直接new了一个LoginUser然后调用他的构造方法将其用户权限传给他。

然后写一个用户权限信息的类和其中获取用户权限的方法,然后返回。

/**
* 用户权限的处理
*/
@Component
public class PermissionService {

   @Autowired
   private MenuMapper menuMapper;

   public List<String> getRolePermision(User user){


       List<String> perms = new ArrayList<>();

//     这边直接写死最高级的管理员id就是为1
       if (user.getId() != null && user.getId() == 1){
           perms.add("*:*:*");
      }
//       其他的用户权限
       else {
//         //根据用户id获得该用户所拥有的权限
           List<String> strings = menuMapper.selectPermsByUserId(user.getId());
           perms.addAll(strings);
      }
       return perms;
  }

}

然后我们回到登录接口调用的登录service实现类,用UserdetailsService的实现类获得该用户的权限后我们需要给前端返回一个token以便后面用户返回接口时控制其权限。这边用jwt来生成token。

    @Override
   public ResponseResult login(User user) {

           //是一个将账号密码封装起来的一个方法
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
           // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername   返回一个LoginUser里面包含账号密码和对应的权限
           Authentication authenticate = authenticationManager.authenticate(authenticationToken);
           
           
//           获取根据用户名查到的信息包括账号密码与权限
           LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
//         生成一个token并且存入到将其和loginuser信息存入到redis中去
           String token = jwtUtil.createToken(loginUser);
           //把token响应给前端
           HashMap<String, String> map = new HashMap<>();
           map.put("token", token);
           return new ResponseResult(200, "登陆成功", map);

  }

这里的步骤是用uuid生成一个随机值,将uuid以键的形式,将用户信息以值的新式存入redis中,然后将uuid传入jwt中让其生成token。

    public  String createToken(LoginUser loginUser) {
       //随机生成一个uuid到时候将uuid以键的形式,将用户信息以值的新式存入redis中
       String token = UUID.randomUUID().toString();
       loginUser.setToken(token);
//       将loginUser以uuid为键存入redis中去,为了方便后面控制登录的用户这里在uuid前加上前缀,相当于分组吧。
       String userKey = "login_tokens_chen:" + token;
       redisCache.setCacheObject(userKey,loginUser);
//       将uuid传入到下一级根据uuid去生成一个token
       Map<String, Object> claims = new HashMap<>();
       claims.put("login_user_key", userKey);
       
       return JwtUtil.createJWT(claims);
  }
    /**
    * 生成jtw
    *
    * @param claims token中要存放的数据(json格式) 是一个键值对 login_user_key:uuid
    * @return
    */
   public static String createJWT(Map<String, Object> claims) {
//       JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间

       String token = Jwts.builder()
              .setClaims(claims)  //将信息封装进token中
              .signWith(SignatureAlgorithm.HS256, generalKey()).compact();
       return token;
  }

然后登录到这基本就结束了。

最后写一个登录的controller去调用我们写的

@RestController
public class LoginController {

   @Resource
   private LoginServcie loginServcie;

   @PostMapping("/user/login")
   public ResponseResult login(@RequestBody User user){

       return loginServcie.login(user);
  }
}

然后用postman对其进行测试。

 

登录成功,可以用token拿去jwt官网解析

 

可以发现我们刚刚的uuid就被存入了token中,然后我们用解析出来的login_user_key去redis中查询用户信息

 

可以看到我们要的用户信息都在redis中可以查询到。

 

过滤器

其实就是将前端的请求链接链接然后解析其token获取用户对应权限

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

   @Autowired
   private JwtUtil jwtUtil;

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
       //获取token
       String token = request.getHeader("token");
       //解析token
       if (!StringUtils.hasText(token)) {  //验证该token不能为null不能为空不能为” “
           //放行
           filterChain.doFilter(request, response);
//           throw new RuntimeException("没有token");
           return;
      } else {
           try {
//               解析前端token获取用户信息
               LoginUser loginUser = jwtUtil.getLoginUser(token);
               // 用于在应用程序中获取当前用户的认证信息
//               SecurityContext context = SecurityContextHolder.getContext();
               //存入SecurityContextHolder
               //获取权限信息封装到Authentication中
               UsernamePasswordAuthenticationToken authenticationToken =
                       new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
               SecurityContextHolder.getContext().setAuthentication(authenticationToken);
          } catch (Exception e) {
               System.out.println("error" + e.getMessage());
          }
      }
       //放行
       filterChain.doFilter(request, response);
  }
}

解析前端token获取用户信息的方法

/**
* 解析前端传入的token获取用户信息
*/
public LoginUser getLoginUser(String token ){
   Claims claims = Jwts.parser()
          .setSigningKey(JWT_KEY)
          .parseClaimsJws(token)
          .getBody();
   // 解析对应的权限以及用户信息
   //获取到了claims就是封装token时的map
   String uuid = (String) claims.get("login_user_key");
   LoginUser user = redisCache.getCacheObject(uuid);
   System.out.println("log jwtUtil:"+ user);
   return user;

}