Spring Security 安全框架NOTE

目录

1、什么是 Spring Security 安全框架?

2、关于 SpringSecurity 中的认证

3、关于 SpringSecurity 中的授权

3.1 从数据库中查询用户的权限信息

4、关于自定义失败处理

5、跨域问题


前提引入:

随着科技的完善,现在几乎所有的网站以及软件都需要进行授权认证,使之更加的安全可靠

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

1、什么是 Spring Security 安全框架?

定义:Spring Security 是一个功能强大且灵活的身份验证和授权框架,用于保护基于 Spring 的应用程序;它提供了一套综合的安全性解决方案,可以用于 Web 应用程序、REST API、微服务等各种应用场景

Spring Security 的主要功能和作用如下:

  1. 身份验证(Authentication):Spring Security 提供了多种身份验证方式,包括基于表单、HTTP 基本认证、LDAP、OAuth2 等。它可以集成到应用程序中,通过验证用户提供的凭据(如用户名和密码)来验证用户身份

  2. 授权(Authorization):Spring Security 支持基于角色和权限的授权机制。它允许您定义细粒度的访问控制规则,例如指定哪些用户具有访问某些受保护资源的权限

  3. 攻击防护(Attack Protection):Spring Security 提供了一系列的防护机制来应对常见的安全攻击,例如跨站点请求伪造(CSRF)攻击、会话固定攻击、点击劫持等。它通过配置合适的安全措施来保护应用程序免受这些攻击的风险

  4. 集成第三方认证系统:Spring Security 可以与其他身份认证系统(如LDAP、OAuth2)进行集成,以便使用这些系统中已有的用户凭据进行身份验证

  5. 定制化和扩展性:Spring Security 提供了丰富的配置选项和可插拔的拦截器机制,使开发人员可以根据应用程序的需求进行灵活的定制和扩展

  6. 审计和日志记录:Spring Security 可以记录关于身份验证和授权过程的审计日志,为应用程序的安全性监控和追踪提供支持


2、关于 SpringSecurity 中的认证

登录校验流程图解:

SpringSecurity 安全框架其实是多个过滤器(过滤器链)所组成的,内部包含了各种功能

SpringSecurity 中过滤器详解图:

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

ExceptionTranslationFilter:处理过滤器链中抛出的任何 AccessDeniedException 和AuthenticationException  异常(即 用户授权异常 和 用户认证异常)

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

认证流程详解图:

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

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

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

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

这里设定一个 UserLogin 类,来继承 UserDetails 接口,用来表示登录用户的信息,便于之后的调用(User 为用户类)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user;


    /**
     * 获取权限信息
     */
    @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 将其放行,使用户在不需要登录的时候也能够访问;同时,可以设置过滤器的前后位置(这里将JWT认证过滤器放在用户登录过滤器之前)

代码如下:

这里是用户登录验证的接口

/**
 * 【用户的验证】
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {

        //1.使用 authenticate 方法进行用户的验证
        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());

        Authentication authenticateResult = authenticationManager.authenticate(authenticationToken);

        //2.如果认证没通过,则进行提示
        if(Objects.isNull(authenticateResult)){

            throw new RuntimeException("用户名或密码错误!");
        }

        //3.如果认证通过,使用 userId 生成一个 jwt
        LoginUser loginUser = (LoginUser) authenticateResult.getPrincipal(); //当前登录用户信息
        Long userId = loginUser.getUser().getId();
        String jwt = JwtUtil.createJWT(userId.toString());  //使用 jwt 工具类进行生成

        HashMap<String, String> map = new HashMap<>();
        map.put("token",jwt);

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

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

}

这里是认证放行接口,同时设置了过滤器的前后顺序

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    /**
     * 【这里是认证放行接口】
     */
    @Bean
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http

                //关闭csrf保护机制
                .csrf().disable()

                //不通过Session获取SecurityContext,而是通过 Token 进行获取
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()

                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()

                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //将 jwt认证过滤器 放在 用户登录过滤器 之前
        http.addFilterBefore(jwtAuthenticationTokenFilter,             
                             UsernamePasswordAuthenticationFilter.class);

    }

}

这里是 JWT认证过滤器,通过请求请求头中发送过来的 Token, 对 Token 中进行解析,从而取出其中的 userId;使用 userId 去 redis 中获取相应的 LoginUser 对象,最后存入 SecurityContextHolder 中(主要用于在整个应用程序中存储和获取用户的安全上下文 )

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        //1.从请求头中获取 token
        String token = request.getHeader("token");
        //1.1 若 token 不存在则直接放行(token 不存在说明不需要认证)
        if(!StringUtils.hasLength(token)){
            chain.doFilter(request,response);
            return;
        }

        //2.解析 token
        String userId ;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token 非法");
        }

        //3.从 redis 中获取用户信息
        String redisKey = "login:"+ userId;

        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){

            throw new RuntimeException("该用户不存在!");
        }

        //4.存入 SecurityContextHolder
        //TODO 获取权限信息
        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(loginUser,null,null);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);


        //5.进行放行
        chain.doFilter(request,response);
    }

前面已经有了用户登录接口,所以现在需要退出登录接口

由于之前在 JwtAuthenticationTokenFilter 过滤器中,已经将登录成功的用户信息放入了 SecurityContextHolder 对象中,所以这里需要将其从中取出;

然后通过用户信息获取到用户 ID,最后在 Redis 中根据对应的用户 ID 删除对应用户的 Redis 缓存信息,这样就可以在下一次登录的时候使之前已经退出的用户 Token 失效,从而需要重新登录,才可访问其他需要授权的页面

/**
     * 【用户退出登录】
     */
    @Override
    public ResponseResult logout() {

        //1.由于用户信息已经保存到了 SecurityContextHolder  中
        //  所以从 SecurityContextHolder 中获取用户 ID
        UsernamePasswordAuthenticationToken authentication
                = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        //1.1 通过 UserDetails 获取用户登录信息
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        User user = loginUser.getUser();
        Long userId = user.getId();

        //2.根据用户 id 删除 redis 中对应的 key 所对应的 value 值
        redisCache.deleteObject("login:"+userId);

        return new ResponseResult(200,"退出登录成功!");
    }

3、关于 SpringSecurity 中的授权

前文引入:

        例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能;但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能

所以说,设置权限这一项,对于一个完整的软件而言尤为重要......

第一步:首先在认证放行接口 SecurityConfig 处开启权限控制

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)  //【开启权限控制】
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  ...............

}

第二步:这里创建一个测试权限的接口,同时设置权限的信息

需要说明的是,@PreAuthorize 注解用于在方法上面设置权限的信息 ,SpEL 表达式定义权限规则,这里的表达式 hasAuthority('test') 表示该方法需要具有"test"权限

@RestController
public class HelloController {

    @GetMapping("hello")
    @PreAuthorize("hasAuthority('system:dept:list')")   //设置权限信息
    public String hello(){

        return "hello";
    }

}

第三步:我们需要重写 LoginUser(继承 UserDetals) 中的 getAuthorities() 方法,将继承 UserDetailService 的类 传过来的权限信息封装到 SimpleGrantedAuthority 对象中进行返回

这里是 UserDetailsServiceImpl 类(继承了 UserDetailsService 类),这里的用户权限信息先写死

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private UserMappper userMappper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper.eq(User::getUserName,username);

        //1.查询用户信息
        User user = userMappper.selectOne(queryWrapper);

        if(Objects.isNull(user)){
            log.error("用户名或密码错误!");
            throw new RuntimeException("用户名或密码错误!");
        }

        //2.将用户信息封装为 UserDetails 对象返回
        ArrayList<String> list = new ArrayList<>(Arrays.asList("test","admin"));    //这里权限信息先进行写死

        return new LoginUser(user,list);    //将用户以及权限信息传入 LoginUser 对象中
    }

}

  这里是 LoginUser 类(继承了 UserDetails 类 );其中, SpringSecurity 中的 SimpleGrantedAuthority     对象用于表示授权信息,表示用户被授予的权限或角色

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private List<String> permissions;   //创建一个集合,用来封装权限信息

    @JSONField(serialize = false)   //敏感信息,让 JSON 字符串不包含该字段
    private List<SimpleGrantedAuthority> authorities;

    /**
     * 获取权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        //1.若不为空,则直接返回(说明之前已经存在权限信息)
        if(authorities!=null){
            return authorities;
        }

        //2.将 permission 中的权限信息封装到 GrantedAuthority 对象中进行返回
        authorities = permissions.stream()
                .map(new Function<String, SimpleGrantedAuthority>() {
                    @Override
                    public SimpleGrantedAuthority apply(String permission) {

                        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
                        return simpleGrantedAuthority;
                    }
                }).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;
    }


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

第四步:由于这时候  LoginUser 中的  getAuthorities() 重写方法已存在用户的权限信息,所以经过 JWT 认证过滤器的时候,需要将其存入 SecurityContextHolder 对象中并进行返回,因为在整个 SpringSecurity 框架中,这个对象是连接整个认证流程的上下文

//4.将用户信息存入 SecurityContextHolder
//4.1获取权限信息
Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();

//4.2 将用户信息以及权限信息存入 SecurityContextHolder 对象中
UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(loginUser,null,authorities);

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

//5.进行放行
chain.doFilter(request,response);

第五步:在用户登录退出接口处,使用该上下文对象,获取当前用户权限信息,来进行后续的操作

这里由于用户的权限信息是写死的,平时通常是动态获取的,所以我们选择从数据库中进行动态的获取权限信息

3.1 从数据库中查询用户的权限信息

这里使用的是 RBAC 模型,即基于角色的权限控制

如图所示:

这里使用角色的关联表将其他的数据表关联起来

  由于存在关联表,所以需要进行多表联查

对应的 SQL 语句如下所示:

SELECT DISTINCT sm.perms
FROM sys_user_role sur  #用户角色关联表 user_role

LEFT JOIN sys_role sr  #角色表 role
ON sur.role_id = sr.id 

LEFT JOIN sys_role_menu srm  #角色权限关联表 role_menu
ON srm.role_id = sur.role_id

LEFT JOIN sys_menu sm  #权限表 menu
ON sm.id = srm.menu_id

WHERE user_id = 2 
AND sr.`status` = 0 AND sm.`status` = 0

MyBatis-Plus 对应的 xml 文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="MyPro.Mapper2.MenuMapper">

    <!-- 根据用户 ID 获取用户的权限信息 -->
    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT DISTINCT (sm.perms)
        FROM sys_user_role sur

                 LEFT JOIN sys_role sr
                           ON sur.role_id = sr.id

                 LEFT JOIN sys_role_menu srm
                           ON srm.role_id = sur.role_id

                 LEFT JOIN sys_menu sm
                           ON sm.id = srm.menu_id

        WHERE user_id = {#userId}
          AND sr.`status` = 0 AND sm.`status` = 0
    </select>

</mapper>

这里进行调用 Mapper 方法,动态的获取用户权限信息

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper.eq(User::getUserName,username);

        //1.查询用户信息
        User user = userMapper.selectOne(queryWrapper);

        if(Objects.isNull(user)){
            log.error("用户名或密码错误!");
            throw new RuntimeException("用户名或密码错误!");
        }

        //2.将用户信息封装为 UserDetails 对象返回
//        ArrayList<String> list = new ArrayList<>(Arrays.asList("test","admin"));    //这里权限信息先进行写死

        List<String> list = menuMapper.selectPermsByUserId(user.getId()); //这里进行动态的获取用户权限信息

        return new LoginUser(user,list);    //将用户以及权限信息传入 LoginUser 对象中
    }

}


4、关于自定义失败处理

前言:

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter 捕获到;在 ExceptionTranslationFilter 中会去判断是认证失败还是授权失败出现的异常

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

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

所以,我们需要进行自定义失败处理,以进行统一的异常处理

这里是【用户认证】的异常处理类:

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用户认证失败!");

        String json = JSON.toJSONString(result);

        //处理异常
        WebUtils.renderString(response,json);
    }

}

这里是【用户授权】的异常处理类:

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {

        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"您的权限不足!");

        String json = JSON.toJSONString(result);

        //处理异常
        WebUtils.renderString(response,json);
    }

}

将上面的异常处理器在 Config 类中进行配置,让 SpringSecurity 框架使用自定义处理器:

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();

        //将 jwt认证过滤器 放在 用户登录过滤器 之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //这里是进行配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)  //用户认证
                .accessDeniedHandler(accessDeniedHandler);  //用户授权
    }


5、跨域问题

前言:

浏览器出于安全的考虑,使用 XMLHttpRequest对象 发起 HTTP请求时必须遵守同源策略(要求源相同才能正常进行通信,即协议、域名、端口号都完全一致,否则就是跨域的HTTP请求,跨域默认情况下是被禁止的

前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题

所以我们就要处理一下,让前端能进行跨域请求

这里对 SpringBoot 配置,进行跨域请求的配置:

@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);

    }

}

当然,以上只是对 Spring 进行了配置跨域的请求,还需要对 SpringSecurity 进行跨域的配置

这里,在 Config 配置类中进行跨域的配置:

@Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                //关闭csrf保护机制
                .csrf().disable()
                //不通过Session获取SecurityContext,而是通过Token进行获取
       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();

        //将 jwt认证过滤器 放在 用户登录过滤器 之前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //这里是进行配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)  //用户认证
                .accessDeniedHandler(accessDeniedHandler);  //用户授权

        //允许跨域
        http.cors();
    }
}

注意:如果使用 PostMan 进行测试是不会成功的,因为它的本质还是在 “同源策略” 中

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值