Spring Security

一、Spring Security基本原理

这里写图片描述

如上图:

1. SecurityContextPersistenceFilter

一个请求和相应的过程为一个线程。

收到请求时,判断session中是否有SecurityContext,如果有则放到当前线程中。请求响应时经过该过滤器,检查当前线程中是否有SecurityContetx,如果有则将其放入session中,这样不同的请求就可以在同一个session中得到相同的信息。SecurityContetx中存放的是当前用户的信息。

2. 认证过滤器

UsernamePasswordAuthenticationFilter:处理表单登录的过滤器;
BasicAuthenticationFilter:处理http basic登录请求的过滤器;

  • 发送登录请求时,检查登录请求中是否带有该过滤器所需的信息。
  • 如果请求中包含用户名和密码,则会进入表单认证的过滤器,如果不包含用户名和密码,则会进入下一个认证过滤器,即Http Basic登录认证的过滤器,如果请求中包含basic开头的Authentication的信息,则做base64解码取出用户名和密码,如果没有继续到下一个认证过滤器中。
  • 任一认证过滤器认证成功,会在请求中添加标记,表示该用户已经认证成功。

3. FilterSecurityInterceptor

经过认证过滤器后来到最后一个过滤器FilterSecurityInterceprot,该过滤器决定当前请求能否访问真正的服务,访问规则在SecurityConfig中配置,如果请求不满足规定的条件,会根据不能访问的原因抛出相应的异常。

    ```
    http.csrf().disable()// 关闭csrf验证
            // 对请求进行认证
            .authorizeRequests()
            // 所有请求需要身份认证
            .anyRequest().authenticated()
            //表单登录
            .and().formLogin()
    ```

4. ExceptionTranslationFilter

捕获FilterSecurityInterceptor抛出的异常,根据捕获的异常类型做相应的处理。eg:因为没有登录而不能访问时,将页面定位到登录页面。

二、认证流程

这里写图片描述

  1. 如果是表单登录,进入UsernamePasswordAuthenticationFilter,处理代码:

    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
    
        String username = obtainUsername(request);
        String password = obtainPassword(request);
    
        ......
    
        //通过用户名和密码构建UsernamePasswordAuthenticationToken对象,其中设置认证标记false,权限列表为空
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
    
        // 将当前请求的信息包括IP、session等放进UsernamePasswordAuthenticationToken中
        setDetails(request, authRequest);
        //认证
        return this.getAuthenticationManager().authenticate(authRequest);
    }
  2. AuthenticationManager: 用来管理AuthenticationProvider。收集所有的AuthenticationProvider,收到请求时,循环它们,判断当前AuthenticationProvider是否支持本次的登录方式,最终找到真正处理用户认证逻辑的AuthenticationProvider。
    ProviderManager中authenticate()的主要代码:

    //本次登录方式
    Class<? extends Authentication> toTest = authentication.getClass();
    ......
    ......
    for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
    
            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }
    
            try {
                result = provider.authenticate(authentication);
    
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            .......
    }
    
  3. AuthenticationProvider:根据用户名获取UserDetails对象,对当前用户进行预检查、密码的验证和后检查,如果检查通过,将用户的权限等信息封装形成Authentication对象。

    实现AuthenticationProvider,执行真正认证逻辑的类DaoAuthenticationProvider。

    主要认证代码:

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
    
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
    
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
    
        if (user == null) {
            cacheWasUsed = false;
    
            try {
                //根据用户名获取UserDetails对象
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            .......
        }
    
        try {
            //用户信息的预检查 (账户是否可用、是否存在、是否过期)
            preAuthenticationChecks.check(user);
            //验证密码
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        .......
        //密码验证成功后,检查用户信息是否已过期(密码等)
        postAuthenticationChecks.check(user);
        //全部验证成功,返回UsernamePasswordAuthenticationToken对象,包括用户的权限等信息,认证标记为true
        return createSuccessAuthentication(principalToReturn, authentication, user);
        }
    
  4. 以上认证全部通过后,在认证过滤器中调用successfulAuthentication()方法,在该方法中将返回的UsernamePasswordAuthenticationToken放入SecurityContext、SecurityContextHolder中,在经过SecurityContextPersistenceFilter过滤器时将SecurityContext放入session中,通过SecurityContextPersistenceFilter可以使认证结果在多个请求中共享。

    获取认证的用户信息:

    SpringMVC会自动在SecurityContext中查找Authentication类型的对象。

    @GetMapping(value = "getUser")
    public Object getUser(Authentication authentication){
        return authentication;
    }

    返回结果:
    这里写图片描述

三、 用户认证的处理

1. 用户信息的获取逻辑

用户信息的获取逻辑封装在UserDetailsService接口中,接口中定义的方法:

    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

作用:根据登录时输入的用户名去数据库中读取用户信息,并封装到UserDetails的实现类User中。如果没有找到对应的用户信息,抛出UsernameNotFoundException异常。

2. 用户校验逻辑

UserDetails接口中声明了关于用户校验的属性,包括用户是否过期、密码是否过期、账户是否锁定等,形成User对象时可以指定这些属性的值。

3. 密码加解密

密码加解密通过实现PasswordEncoder接口来完成。该接口中包含两个方法

  • 加密方法:用户注册时调用该方法,将加密后的密码存进数据库中。
String encode(CharSequence rawPassword);
  • 校验方法:对数据库中保存的密码和页面上填写的密码进行校验。Spring Security在用户登陆,生成User对象后,将User中的密码和登陆请求中的密码进行比对,如果比对不成功,该方法返回false,Spring Security抛出异常。
boolean matches(CharSequence rawPassword, String encodedPassword);

四、 自定义登陆功能

  1. 如下代码:
        // 关闭csrf验证
        http.csrf().disable()
                //对请求授权
                .authorizeRequests()
                // 登陆请求允许全部访问,如果没有该配置会形成死循环,提示重定向次数过多
                .antMatchers("/signIn.html").permitAll()
                // 所有请求需要身份认证
                .anyRequest().authenticated()
                //设置表单登录,登陆页面为/signIn.html
                .and().formLogin().loginPage("/signIn.html")
                //设置自定义的处理登陆的post请求
                .loginProcessingUrl("/authentication/form");

五、添加记住我功能

  1. 记住我的基本原理
    这里写图片描述

    RememberMeAuthenticationFilter在过滤器链中在认证过滤器之后。

    登陆认证成功之后,执行认证过滤器中的successfulAuthentication()方法,在该方法中执行

    rememberMeServices.loginSuccess(request, response, authResult);

    RememberMeServices的实现类PersistentTokenBasedRememberMeServices中该方法的具体实现为:

    protected void onLoginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                username, generateSeriesData(), generateTokenData(), new Date());
        try {
            //生成token,并保存在数据库中
            tokenRepository.createNewToken(persistentToken);
            //将token保存在cookie中
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }

    登陆成功后,退出此次登陆,再次发送请求时经过认证过滤器后,会被RememberMeAuthenticationFilter拦截,调用autoLogin()方法,在该方法中通过tokenRepository根据cookie中的token获取数据库中存放的token信息,验证通过后,根据token中的用户名获取用户信息并返回。

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        //在该过滤器之前没有认证过,调用rememberMeServices的自动登录方法
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
                    response);
            if (rememberMeAuth != null) {
                try {
                    rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
                    SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
                    onSuccessfulAuthentication(request, response, rememberMeAuth);
                    }
            }
    
            ......
    
        }
        else {
            ......
            chain.doFilter(request, response);
        }
    }

    TokenRepository的作用:将token写入数据库中、用户再次访问请求时在数据库中查找当前token对应的用户名。

  2. 系统实现

    (1)统一实现

    配置TokenRepository
    设置token的过期时间,以秒为单位
    TokenRepository获取到用户名之后,通过myUserDetailsService获取用户信息,完成再一次的请求操作

    .rememberMe()
        .tokenRepository(persistentTokenRepository())
        .tokenValiditySeconds(3600)
        .userDetailsService(myUserDetailsService)

    (2) TokenRepository的配置

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //设置数据源
        tokenRepository.setDataSource(dataSource);
        //系统启动时自动创建表
        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    (3) 在页面上增加记住我的复选框,name属性固定为remember-me,如果要修改,同时修改SecurityCongfig中的rememberMeParameter属性的值。

    <tr>
        <td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
    </tr>

    上述配置完成以后,启动服务,在对应的数据库中会自动创建表persistent_logins,在登陆页面选中‘记住我‘登陆后会向该表中写入一条数据,包括用户名、token等信息,同时会将token保存在Cookie中。
    退出本次登陆,再次访问受保护的请求时,TokenRepository会根据Cookie中保存的Token查询persistent_logins中对应的记录,获取到用户名,根据用户名获取该用户的权限等信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值