SpringSecurity深度学习

2024-01-07 22:37:27

?SpringSecurity简介

spring Security是什么?

Spring Security 是一个强大且高度可定制的身份验证和访问控制框架,用于保护基于Spring的应用程序。它是Spring项目的一部分,旨在为企业级系统提供全面的安全性解决方案。

一个简单的授权和校验流程

检验流程

总流程?

SpringSecurity使用

认证?

对应依赖

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

在自定义检验的时候,主要就是实现UserDetailsService接口,重写loadUserByUserName方法,在该方法中就是去检验账号和密码的准确性。(一般都是进行数据库的查询校验,默认的密码格式就是 ·{}密码·)

前后端分离项目登录流程

1.在springSecurity中我们的需要设置密文的配置,在项目中大多都是使 BCryptPasswordEncoder类来做密码的加密。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


}

2.创建对应的login接口和service

Controller?

@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    LoginService loginService;

    @PostMapping("/login")
    public ResponseResult login(@RequestBody User user) {
        return loginService.login(user);
    }
}

Service

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    RedisCache redisCache;
    @Override
    public ResponseResult login(User user) {
        //authenticationManager authenticate进行用户验证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //执行我们对饮的认证方法,在该方法中会返回LoginUser类型的数据
        //如果没有通过通过直接抛异常
        if(ObjectUtil.isEmpty(authenticate)) {
            throw new RuntimeException("登录失败");
        }
        //如果成功直接生成token,将其也map返回
        LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
        String jwt = JwtUtil.createJWT(loginUser.getUser().getId().toString());
        Map<String, String> data = new HashMap<>();
        data.put("token", jwt);
        redisCache.setCacheObject(loginUser.getUser().getId().toString(), user);
        //返回token
        return new ResponseResult(200, "登录成功", data);
    }
}

因为AuthenticationManager默认不在ioc中,我们需要将其配置到ioc中,并且配置对应的校验规则。在里面就包括 无效校验的接口(比如:登录接口)和其他一些前后端分离的配置。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //将AuthenticationManager配置到ioc中
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //由于是前后端分离项目,所以要关闭csrf
                .csrf().disable()
                //由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //指定让spring security放行登录接口的规则
                .authorizeRequests()
                // 对于登录接口 anonymous表示允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }
}

进行测试,校验成功。

前后端分离项目校验流程

1.创建一个校验过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token,token会存在header中
        String token = request.getHeader("token");
        if(StrUtil.isEmpty(token)) {
            //由后续的拦截器进行拦截
            filterChain.doFilter(request, response);
            //后续会返回回来,需要return,不然会执行下面的语句
            return ;
        }
        //解析token
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
            if(StringUtil.isNullOrEmpty(userId)) {
                throw new RuntimeException("token解析失败");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        //从redis中获取用户的信息
        LoginUser loginUser = redisCache.getCacheObject(userId);
        if(ObjectUtil.isEmpty(loginUser)) {
            throw new RuntimeException("Redis中没有用户信息");
        }
        //将数据存储到SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

使用三个参数的UsernamePasswordAuthenticationToken的构造器,该构造器会设置授权成功。

2.将过滤器设置到用户验证过滤器之前

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    //设置加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //将AuthenticationManager配置到ioc中
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //由于是前后端分离项目,所以要关闭csrf
                .csrf().disable()
                //由于是前后端分离项目,所以session是失效的,我们就不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //指定让spring security放行登录接口的规则
                .authorizeRequests()
                // 对于登录接口 anonymous表示允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();


        //将过滤器添加到用户登录处理器之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

进行测试,将成功的token放入header中,进行校验。最终校验成功。

退出登录流程

1.编写退出登录接口

@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    LoginService loginService;


    @RequestMapping("/logout")
    public ResponseResult logout() {
        return loginService.logout();
    }
}

2.编写service实现类,删除redis中用户信息的数据,即可完成退出登录操作。在解析的时候redis中的数据不存在就会直接被拦截。

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    RedisCache redisCache;


    @Override
    public ResponseResult logout() {
        //在进入此接口时会先进行解析,成功之后才会执行logout,此时SecurityContextHolder中是有用户信息的
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        if(ObjectUtil.isEmpty(loginUser)) {
            throw new RuntimeException("LoginUser不存在");
        }
        //把redis中的数据删除之后,下次解析的时候就会直接报错,在解析中我们对redis的数据做了判空的操作
        redisCache.deleteObject(loginUser.getUser().getId().toString());
        return new ResponseResult(200, "退出登录成功", null);
    }
}

进行测试,最终成功。

授权

1.开启授权功能,在对应的security的配置类中添加对应的注解。

@EnableGlobalMethodSecurity(prePostEnabled = true) //开启授权

2.为接口设置对应的权限需求

@RestController
public class baseController {

    @RequestMapping("/hello")
    //拥有text倾向才能访问
    @PreAuthorize("hasAuthority('text')")
    public String hello() {
        return "hello!";
    }
}

3.在用户进行认证的时候查询用户拥有的权限集合,并设置到 authenticationToken中。

  • 在返回类型中设置权限集合属性和重写获取权限集合方法。
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private List<String> authorities2String;

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

    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(CollUtil.isEmpty(authorities)) {
            return authorities2String.stream()
                    .map(item -> new SimpleGrantedAuthority(item))
                    .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;
    }
}
  • 在校验Service中查询对应的权限列表。
@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.huang.springsecuritydemo.entity.User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("user_name", username);
        com.huang.springsecuritydemo.entity.User user = userMapper.selectOne(userQueryWrapper);
        if(ObjectUtil.isEmpty(user)) {
            throw new RuntimeException("用户不存在");
        }
        //todo 查询并设置对应的权限信息
        //模拟查到的权限信息
        List<String> data = Arrays.asList("test", "text");

        return new LoginUser(user, data);
    }
}
  • 在JWT认证中向authenticationToken中设置权限集合,最终设置到SecurityContextHolder中。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token,token会存在header中
        String token = request.getHeader("token");
        if(StrUtil.isEmpty(token)) {
            //由后续的拦截器进行拦截
            filterChain.doFilter(request, response);
            //后续会返回回来,需要return,不然会执行下面的语句
            return ;
        }
        //解析token
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
            if(StringUtil.isNullOrEmpty(userId)) {
                throw new RuntimeException("token解析失败");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        //从redis中获取用户的信息
        LoginUser loginUser = redisCache.getCacheObject(userId);
        if(ObjectUtil.isEmpty(loginUser)) {
            throw new RuntimeException("Redis中没有用户信息");
        }
        //将数据存储到SecurityContextHolder
        //todo 设置对应的权限信息
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

进行测试,授权成功。

RDAB模型例子(基本通用,看进行二次开发)

1.创建五个数据库 用户表,角色表,权限表,用户角色关联表,角色权限关联表。

2.编写SQL语句查询用户的所有权限,并使用 mybatis-plus进行封装为一个函数进行调用。

SELECT
            DISTINCT m.`perms`
        FROM
            sys_user_role ur
                LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
                LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
                LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            user_id = #{id}
          AND r.`status` = 0
          AND m.`status` = 0

3.在校验是进行调用,并返回对应的权限集合。

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;
    @Autowired
    MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<com.huang.springsecuritydemo.entity.User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("user_name", username);
        com.huang.springsecuritydemo.entity.User user = userMapper.selectOne(userQueryWrapper);
        if(ObjectUtil.isEmpty(user)) {
            throw new RuntimeException("用户不存在");
        }
        //todo 查询并设置对应的权限信息
        List<String> data = menuMapper.selectPermsByUserId(user.getId());

        return new LoginUser(user, data);
    }
}

修改接口所需要的权限。

@RestController
public class baseController {

    @RequestMapping("/hello")
    //拥有text倾向才能访问
    @PreAuthorize("hasAuthority('system:test:list')")
    public String hello() {
        return "hello!";
    }
}

进行测试,最终成功。

自定义失败处理

1.自定义授权异常处理器和校验异常处理器。

  • 校验异常处理器
//校验失败异常处理器
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //创建统一的返回对象,设置到response中
        ResponseResult responseResult = new ResponseResult(HttpStatus.HTTP_UNAUTHORIZED, "校验失败!");
        String json = JSON.toJSONString(responseResult);
        //将统一的结果设置到Response中,本质就是将对应的数据设置到response中
        WebUtil.renderString(response, json);
    }
}
  • 授权异常处理器

//授权失败异常处理器
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult responseResult = new ResponseResult(HttpStatus.HTTP_UNAUTHORIZED, "授权失败!");
        String json = JSON.toJSONString(responseResult);
        WebUtil.renderString(response, json);
    }
}

对应的webUtil工具类

public class WebUtil {
    /**
     * 将字符串渲染到客户端
     *
     * @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");
            //将结果json以流的形式写入response中
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}

2.将自定义的异常处理器进行配置

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //将AuthenticationManager配置到ioc中
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //由于是前后端分离项目,所以要关闭csrf
                .csrf().disable()
                //由于是前后端分离项目,没有session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //指定让spring security放行登录接口的规则
                .authorizeRequests()
                // 对于登录接口 anonymous表示允许匿名访问, permitAll就是 登录和没登录都可以访问
                .antMatchers("/user/login").anonymous() //匿名访问,未登录就可以访问
                // 除上面外的所有请求全部需要鉴权认证后访问
                .anyRequest().authenticated();
        //将过滤器添加到用户登录处理器之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //设置自定义的异常处理器
        http.exceptionHandling()
                //校验异常处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                //授权异常处理器
                .accessDeniedHandler(accessDeniedHandler);
    }
}

进行测试,异常显示成功。

允许跨域

1.开启springboot的允许跨域。

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    //重写spring提供的WebMvcConfigurer接口的addCorsMappings方法
    public void addCorsMappings(CorsRegistry registry) {
        //设置可以跨域的映射地址
        registry.addMapping("/**")
                // 设置可以跨域的源
                .allowedOriginPatterns("*")
                // 是否允许使用cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

2.开启springsecurity的允许跨域。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //将AuthenticationManager配置到ioc中
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //由于是前后端分离项目,所以要关闭csrf
                .csrf().disable()
                //由于是前后端分离项目,没有session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //指定让spring security放行登录接口的规则
                .authorizeRequests()
                // 对于登录接口 anonymous表示允许匿名访问, permitAll就是 登录和没登录都可以访问
                .antMatchers("/user/login").anonymous() //匿名访问,未登录就可以访问
                // 除上面外的所有请求全部需要鉴权认证后访问
                .anyRequest().authenticated();
        //将过滤器添加到用户登录处理器之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //设置自定义的异常处理器
        http.exceptionHandling()
                //校验异常处理器
                .authenticationEntryPoint(authenticationEntryPoint)
                //授权异常处理器
                .accessDeniedHandler(accessDeniedHandler);
        //允许跨域
        http.cors();
    }
}

最终设置完成。

自定义权限校验方法(比较灵活,可以自定义策略)

1.自定义校验类

@Component("itolen") //设置该类在ioc中的名称
public class ExpressionRoot {

    //判断权限是否存在
    public boolean hasAuthority(String authority) {
        LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        List<String> authorities2String = loginUser.getAuthorities2String();
        return authorities2String.contains(authority);
    }
}

2.在对应的接口上调用自定义方法。

@RestController
public class baseController {

    @RequestMapping("/hello")
    //拥有text倾向才能访问
    @PreAuthorize("@itolen.hasAuthority('system:test:list')")
    public String hello() {
        return "hello!";
    }
}

进行测试。

?其他处理器?

  • 认证成功处理器和认证失败处理器
//认证成功处理器实现类
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //认证成功后就会进行该方法
        System.out.println("认证成功!");
    }
}
//认证失败处理器实现类
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        //认证失败后执行该方法
        System.out.println("认证失败!");
    }
}

?将两个类进行配置。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    //配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置校验成功处理器和校验失败处理器
        http.formLogin().successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler);
    }
}
  • 注销成功处理器
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //注销成功后执行的方法
        System.out.println("注销成功!");
    }
}

将该类进行配置。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

 
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;

  
    //配置权限规则,主要就睡要放行登录接口,不然登录接口都会被了解,以及其他不要的前后端分离的配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        //设置注销成功处理器
        http.logout().logoutSuccessHandler(logoutSuccessHandler);
    }
}

文章来源:https://blog.csdn.net/Ostkakah/article/details/135428558
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。