1.认证
密码校验用户

密码加密存储

            
            
              java
              
              
            
          
          @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}- 我们没有以上代码配置,默认明文存储, {id}password;
- 实现这个将passworxEncode变成@Bean之后,就变成加密存储: 加盐值 + 明文随机生成一段密文;
- 将返回UserDetails对象中的密码,与输入的密码进行校验
登录接口

- 在SpringSecurity放行登录接口,
- 定义AuthenticationManager @bean
            
            
              java
              
              
            
          
          @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}PS:就我们研究框架, 有些值,需要我们利用断点调试,层层拨开来进行获取。

我们创建一个自定义登录接口,并把内部逻辑交给自定义UserService的实现类的方法来处理。
            
            
              java
              
              
            
          
          @Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;
    @Override
    public ResponseResult login(UserDTO userDTO) {
        //  AuthenticationManager authenticationManager 进行认证
        UsernamePasswordAuthenticationToken authenticationToken = 
                new UsernamePasswordAuthenticationToken(userDTO.getUsername(), userDTO.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 如果认证通过、给出对应提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败");
        }
        // 认证通过使用userid生成jwt
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String token = JwtUtil.createJWT(userId);
        HashMap<String, String> map = new HashMap<>();
        map.put("token",token);
        // 把完整用户信息存入redis
        redisCache.setCacheObject("login:"+userId,loginUser);
        return new ResponseResult(200,"登录成功",map);
    }
}认证过滤器

            
            
              java
              
              
            
          
          @Component
public class JwtFilter extends OncePerRequestFilter {
    @Autowired
    private RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("Authorization").substring(6);
        if(StringUtils.isEmpty(token)){
            filterChain.doFilter(request,response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}由上面配置,我们得知:
- 假如没有token,token过滤器放行,但是还会经理SpringSecurity过滤器链,由于SecurityContextHolder没有用户信息,过滤器链判断未认证;
- login接口,tokne过滤器也是没有toke 放行,但是我们SpringSecurity过滤器链是默认放行的。
            
            
              java
              
              
            
          
          @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private JwtFilter jwtAuthenticationTokenFilter;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        //把token校验过滤器添加到过滤器链中,就是在 UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}退出接口
- 对于每次请求都是不同的线程。
- 同一个运行程序,一般的静态变量可以被不同线程访问;
- ThreadLocal的静态变量,是线程隔离,只有当前线程才能访问;
- 因此,每次访问的SecurityContextHolder默认是空的,只有经过jwt过滤器之后,变得有意义;

2.授权
我们前面是基于认证的流程,但是实际上,我们认证之后,不同等级用户,需要不同的权限访问。
- 从SpringSecurityInterceptor进行权限校验,那么我们要配置让他权限校验。

限制接口访问权限
springSecurity开启权限配置,1.注解形式 2.配置文件(java配置类)形式

封装权限信息

UserDetailService修改:

UserDetails修改:
            
            
              java
              
              
            
          
          @Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private User user;
    private List<String> authList;
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;  // SimpleGrantedAuthority对象不支持序列化,无法存入redis
    
    public LoginUser(User user, List<String> authList) { // 将对应的权限字符串列表传入
        this.user = user;
        this.authList = authList;
    }
     @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;
        if(authorities != null){
            return authorities;
        }else{
            authorities = new ArrayList<>();
        }
        // 第一次登录,封装UserDetails对象,初始化权限列表
        for (String auth : authList) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);
            authorities.add(simpleGrantedAuthority); // 对,默认是个空的
        }
        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;
    }
}认证过滤器更改:将权限信息增加到contex中,以便后续拦截器链获取。

从数据库查询信息
RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

涉及到的sql语句
通过多表查询的形式,将符合条件字段全部查出,筛选出权限字段。
            
            
              sql
              
              
            
          
          SELECT 
				m.perms
FROM
	`sys_user_role` as ur
	LEFT JOIN `sys_role` as r on ur.role_id = r.id
	LEFT JOIN `sys_role_menu` as rm on ur.role_id = rm.role_id
	LEFT JOIN `sys_menu` as m on rm.menu_id = m.id
WHERE 
	ur.`user_id` =2 AND r.`status`=0 and m.`status`=0;我们之前UserDetail实现对象LoginUser的权限列表存静态权限,现在改为从数据库查询:

测试权限接口改为:
            
            
              java
              
              
            
          
          @RestController
public class TestController {
    @GetMapping("/hello")
    @PreAuthorize("hasAuthority('system:test:list')")
    public String hello(){
        return "hello";
    }
}3.自定义失败处理

上面流程,使用postman进行测试的时候出现错误,springboot程序会直接错误响应如403、401;
但是我们希望程序正确响应,403等响应结果是由我们自定义公共对象封装而成。
WebUtil工具类
            
            
              java
              
              
            
          
          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().write(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}AuthenticationEntryPoint实现
            
            
              java
              
              
            
          
          @Component
public class AuthenticationEntryPointImpl  implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult<Object> responseResult =
                new ResponseResult<>(401, "您认证错误/请检查你的用户名或密码是否正确");
        String jsonResult = JSON.toJSONString(responseResult);
        WebUtils.renderString(response,jsonResult);
    }
}AccessDeniedHandler实现
            
            
              java
              
              
            
          
          @Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
   
        ResponseResult<Object> responseResult =
                new ResponseResult<>(403, "权限不足");
        String jsonResult = JSON.toJSONString(responseResult);
        WebUtils.renderString(response,jsonResult);
    }
}在SecurityConfig中加入下配置

4.跨域
前后端分离项目,都存在一个问题,跨域。
凡是,我们后端能就接受到前端原生response和request对象的都需要解决跨域问题。
SpringMvc跨域解决
            
            
              java
              
              
            
          
          @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);
    }
}SpringSecurity跨域解决
在security配置类加上如下配置。

原生设置
            
            
              java
              
              
            
          
          // 设置跨域
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 修改携带cookie,PS
        response.setHeader("Access-Control-Allow-Methods", "GET");
        response.setHeader("Access-Control-Allow-Headers", "Authorization,content-type"); // PS
        // 预检请求缓存时间(秒),即在这个时间内相同的预检请求不再发送,直接使用缓存结果。
        response.setHeader("Access-Control-Max-Age", "3600");