Spring Security

410 阅读10分钟

Spring Security - 我爱学习网 (5axxw.com)

底层是filter,经过一系列filter,对请求进行处理,最终到达servlet。 共有11个过滤器。

组件

SecurityContext :上下文对象,存储认证后的Authenation 。

SecurityContextHolder :用于获取SecurityContext 的工具类。

AuthenticationManager :用于校验Authentication,返回一个认证完成后的Authentication对象,默认实现类是ProviderManager。 Authentication : 用户的认证信息,有三个核心属性:

  1. principal: 用户的身份信息
  2. credentails: 用户认证凭据,比如密码。认证完成后,此项会被清空。
  3. authorities:用户权限。

Authentication的两个主要作用:

  1. 为 AuthenticationManager 对象提供用于认证的信息载体;
  2. 用于获取某个用户的基本信息。

image.png

在 Spring Boot 方式下启动 Spring Security 工程,将会自动开启如下配置项:

  • 默认开启一系列基于 springSecurityFilterChain 的 Servlet 过滤器,包含了几乎所有的安全功能,例如:保护系统 URL、验证用户名、密码表单、重定向到登录界面等;

  • 创建 UserDetailsService 实例,并生成随机密码,用于获取登录用户的信息详情;

  • 将安全过滤器应用到每一个请求上。

配置类

继承WebSecurityConfigurerAdapter, 加注解 定义类AuthenticationManager

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }




    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/register", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()
                // 除上面外的所有请求全部需要认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();

        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);


        // 验证token的拦截器放在UsernamePasswordAuthenticationFilter之前
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
//        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
//        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 对密码进行加密    非明文加密   用户输入的密码传来后被security加密,之后判断用的是加密的密码
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
//        密码加密
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

自定义UserDetail实现类

public class LoginUser implements UserDetails {


    private Long userId;
    private Long deptId;

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

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 登录IP地址
     */
    private String ipaddr;

    /**
     * 登录地点
     */
    private String loginLocation;

    /**
     * 浏览器类型
     */
    private String browser;

    /**
     * 操作系统
     */
    private String os;

//    权限列表
    private Set<String> permissions;
    /**
     * 用户信息
     */
    private SysUser user;

自定义UserDetailsService实现类

public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleInfoService roleInfoService;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.debug("开始登陆验证,用户名为: {}",s);

        // 根据用户名验证用户
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(UserInfo::getLoginAccount,s);
        UserInfo userInfo = userService.getOne(queryWrapper);
        if (userInfo == null) {
            throw new UsernameNotFoundException("用户名不存在,登陆失败。");
        }

        // 构建UserDetail对象
        UserDetail userDetail = new UserDetail();
        userDetail.setUserInfo(userInfo);
        List<RoleInfo> roleInfoList = roleInfoService.listRoleByUserId(userInfo.getUserId());
        userDetail.setRoleInfoList(roleInfoList);
        return userDetail;
    }
}

认证过程中SpringSecurity会调用这个方法查询出用户,验证登录用户信息是否正确。将我们查询出来的用户信息和权限信息组装成一个UserDetails返回。

认证过滤器

自定义未授权处理

自定义退出处理

注解

@EnableWebSecurity作用 组合注解。

@Import({EnableGlobalMethodSecurity})激活了websecurityConfiguration配置类。 注入了一个非常重要的bean,bean的name为springSecurityFilterChain,这是springsecurity的过滤器,是请求的认证入口。

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter

@EnableGlobalAuthentication

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({GlobalMethodSecuritySelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {

    boolean prePostEnabled() default false;  开启后支持springEL表达式。

    boolean securedEnabled() default false;

    boolean jsr250Enabled() default false;  

    boolean proxyTargetClass() default false;
 
    AdviceMode mode() default AdviceMode.PROXY;

    int order() default 2147483647;
}

这个注解中引入了人AuthenticationConfiguration配置类,这个类用于配置认证相关的类, 向spring容器中注入了AuthenticationManagerBuilder,AuthenticationManagerBuilder使用了建造者模式。 所以@EnableWebSecurity有两个作用: 1.加载websecurityConfigurstion配置类,配置安全认证策略。 2.加载了AuthenticationConfigurstion配置了认证信息。

过滤器是如何加入到过滤器链中的

以FilterComparator提供的规则进行比较,按照比较结果进行排序注册。 通过HttpSecurity的addFilter方法加入到过滤器链中。

@PreAuthorize("@ss.hasPermi('system:user:resetPwd')")

在方法执行之前就起作用。如果满足条件就能执行方法。

支持自定义的权限判断。 ss.hasPermi() 就是自定义了一个类,并且由自定义的判断权限的方法,返回true或false

认证

表单认证

表单认证是最常用的一个认证方式,一个最直观的业务场景便是用账号密码登录,UsernamePasswordAuthenticationFilter,在整个认证体系中至关重要。 登陆后进入UsernamePasswordAuthenticationFilter,进入attemptAuthentication方法中,拿到用户名和密码,

鉴权

http无状态,所以每次访问都是一个新的请求,那么服务器就只能根据token来判断你是谁。 每次请求都会被JwtAuthenticationTokenFilter拦截,取出请求头中的token。 根据token获取用户信息,然后创建一个UsernamePasswordAuthenticationToken,把它存到security的上下文,这个就是授权用户,否则就是匿名用户。 后续的过滤器就能让请求通过。否则请求会被拦住。

用户的权限信息会存到authentication。 后续请求会从authentication中获取权限进行判断。

运行中,会调用hasAuthority()方法进行校验. hasAuthority可以自己重写.

springsecurity还为我们提供了其他方法,hasanyauthoritity,hasrole,hasanyrole.

hasanyauthoritity可以指定多个权限.具有其中一个,就可以访问

hasAuthority指定一个权限

hasrole可以访问的角色

hasanyrole多个可以访问的角色。

过滤器执行过程

SecurityContextHolderAwareRequestFilter

FilterSecurityInterceptor 权限验证,验证不通过会抛出异常; ExceptionTransLationFilter处理抛出的异常 。 AnonymousAuthenticationFilter 如果security上下文中没有认证信息Authentication,就给上下文添加一个匿名用户,防止后面过滤器运行时发生空指针异常。

配置动态权限:

RBAC

用户角色权限模型。 根据模型的复杂程度,可分为RBAC0,RBAC1,RBAC2,RBAC3

RBAC0:用户角色权限之间是多对多 RBAC1:角色分成了多个等级

用户组:直接给用户组(部门)分配角色,再把用户加入用户组,这样用户不仅有自己的角色,还有所属用户组的角色。

权限字符串

system:user:list 自定义。 模块:表:操作

jwt

服务器如何记住用户?靠的是会话跟踪。常用的会话跟踪技术是cookie,session,token。 cookie通过客户端记录信息,session通过服务端记录信息。 web程序是基于HTTP协议传输数据的,而HTTP协议是无状态的,每次请求完后都会断开连接,再次请求需要重新建立连接。这就意味着服务器无法根据连接来跟踪会话。 那就需要特殊的机制。 无状态请求:服务端处理的信息是服务端保存的 有状态请求:服务端处理的信息来自于请求所携带的 有状态的服务常用于实现事务,如添加购物车,付款时从购物车获取商品信息。 无状态服务,为了伸缩性考虑,实现服务端水平扩展。请求就可以发到任意一个服务器。

同源策略: 同源指两个页面具有相同的协议,主机和端口号。 发生跨域时,cookie是不起作用的。

跨域: CORS 普通跨域请求:只需服务端设置。

带cookie的跨域:前后端都进行设置。 服务端在response中配置了,浏览器检测到,就能允许ajax进行跨域访问。

  • 服务端设置:response.setHeader("Access-Control-Allow- origin","*"); 允许跨域访问的域名,*表示任何请求都可以跨域

  • response.setHeader("Access-Control-Allow-Credentials","true"); 允许前端带认证cookie,启用此项后,上面的域名不能为*

  • response.seteader("Access-control-Allow-Headers","content-type,X-Requested-with");

cookie的常见场景: 记录用户的一些信息

识别session,需要客户端携带的cookie 这个cookie是自动生成的,仅当前浏览器内有效。 session在分布式下不好。

服务端不保存用户信息, 在token中保存一些用户相关的信息,发给客户端。 客户端请求时携带token字符串,服务端解析token,去查询用户(可以用数据库也可以用redis。最好redis),没查到用户信息说明未登录。

token由三部分数据组成: header:。 payload:通常会保存一些与用户相关的信息,如用户名;存储过期时间。但是不能放太多信息。 signature:header+payload+secret

secret:令牌秘钥 生成签名的时候使用,secret是服务端的,不能暴露出去。

token存在哪里? cookie不建议(CSRF攻击) http请求头。 CSRF攻击:攻击者伪造请求,并获取cookie,这样攻击者的请求就能通过验证。 解决方法是在请求中放入不能伪造的信息,并且不能存在cookie中,因此可以使用token,在后端拦截请求验证token。

CSRF攻击过程: 用户访问正规网站A,网站A生成cookie,并保存在用户浏览器中。 攻击者在网站A发布一些信息并加上网站B的链接,诱导用户访问网站B. 可以认为是钓鱼网站。 用户访问了网站B,B网站就获取了用户发过来的cookie 网站B携带着用户的cookie去访问网站A,网站A就会响应 防护措施:

TokenUtil

采用JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了:

创建token 验证token 反解析token中的信息 做JWT认证需要我们自己写一个过滤器来做JWT的校验,然后将这个过滤器放在UsernamePasswordAuthenticationFilter前面。

@Override
    protected void doFilterInternal(@NotNull HttpServletRequest request,
                                    @NotNull HttpServletResponse response,
                                    @NotNull FilterChain chain) throws ServletException, IOException {
        log.info("JWT过滤器通过校验请求头token进行自动登录...");

        // 拿到Authorization请求头内的信息
        String authToken = jwtProvider.getToken(request);

        // 判断一下内容是否为空且是否为(Bearer )开头
        if (StrUtil.isNotEmpty(authToken) && authToken.startsWith(jwtProperties.getTokenPrefix())) {
            // 去掉token前缀(Bearer ),拿到真实token
            authToken = authToken.substring(jwtProperties.getTokenPrefix().length());

            // 拿到token里面的登录账号
            String loginAccount = jwtProvider.getSubjectFromToken(authToken);

            if (StrUtil.isNotEmpty(loginAccount) && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 缓存里查询用户,不存在需要重新登陆。
                UserDetail userDetails = caffeineCache.get(CacheName.USER, loginAccount, UserDetail.class);

                // 拿到用户信息后验证用户信息与token
                if (userDetails != null && jwtProvider.validateToken(authToken, userDetails)) {

                    // 组装authentication对象,构造参数是Principal Credentials 与 Authorities
                    // 后面的拦截器里面会用到 grantedAuthorities 方法
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());

                    // 将authentication信息放入到上下文对象中
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    log.info("JWT过滤器通过校验请求头token自动登录成功, user : {}", userDetails.getUsername());
                }
            }
        }

        chain.doFilter(request, response);
    }

从请求中取出token->解析token->token认证->security认证 登录->拿到token->请求带上token->JWT过滤器拦截->校验token->从缓存里面拿我们的UserDetail-> 组装一个authentication对象,把它放在上下文对象中,这样后面的过滤器看到我们上下文对象中有authentication对象,就相当于我们已经认证过了

Oauth2

一个关于授权的开放网络标准。

不使用本系统的账号密码,同意第三方应用进入系统,系统会生成一个短期的token,供第三方使用。 一些网站允许使用其他方式登录,qq,微信等,就是第三方登录。 token和密码 token是临时的,到期失效 token可以被所有者撤销。 token有权限范围。