SpringSecurity学习

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");
相关推荐
LN-ZMOI15 分钟前
c++学习笔记1
c++·笔记·学习
五味香19 分钟前
C++学习,信号处理
android·c语言·开发语言·c++·学习·算法·信号处理
无理 Java24 分钟前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc
gobeyye1 小时前
spring loC&DI 详解
java·spring·rpc
鱼跃鹰飞1 小时前
Leecode热题100-295.数据流中的中位数
java·服务器·开发语言·前端·算法·leetcode·面试
云端奇趣1 小时前
探索 3 个有趣的 GitHub 学习资源库
经验分享·git·学习·github
我是浮夸1 小时前
MyBatisPlus——学习笔记
java·spring boot·mybatis
我感觉。1 小时前
【信号与系统第五章】13、希尔伯特变换
学习·dsp开发
TANGLONG2221 小时前
【C语言】数据在内存中的存储(万字解析)
java·c语言·c++·python·考研·面试·蓝桥杯
杨荧1 小时前
【JAVA开源】基于Vue和SpringBoot的水果购物网站
java·开发语言·vue.js·spring boot·spring cloud·开源