欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

2 Spring Security详解(认证用户)

程序员文章站 2024-02-15 13:13:52
...

认证用户的过程:进入认证页面-->输入用户名和密码-->CSRF认证-->查询存储的用户数据(用户名、密码以及角色信息)-->认证完成

1 自定义认证页面

不使用Spring Security自带的认证页面,使用自己定义的。

  • 释放静态资源,拦截器不要拦截静态资源。
  • 匿名访问是要允许的,因为认证失败要可以跳转到认证页面。
  • 配置登录页面,默认页面,登录失败页面。
        <!--释放静态资源-->
        <security:http pattern="/css/**" security="none"/>
        <security:http pattern="/img/**" security="none"/>
        <security:http pattern="/plugins/**" security="none"/>
        <security:http pattern="/failer.jsp" security="none"/>
        <security:http auto-config="true" use-expressions="true">
        
        <!--拦截资源-->
        <!--让认证页面可以匿名访问-->
        <security:intercept-url pattern="/login.jsp" access="permitAll()"/>
        <!--
        pattern="/**" 表示拦截所有资源
        access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
        -->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
        <!--配置认证信息-->
        <security:form-login login-page="/login.jsp"
                             login-processing-url="/login"
                             default-target-url="/index.jsp"
                             authentication-failure-url="/failer.jsp"/>
        <!--配置退出登录信息-->
        <security:logout logout-url="/logout"
                         logout-success-url="/login.jsp"/>

    </security:http>

 再次启动项目后就可以看到自定义的酷炫认证页面了!

2 Spring Security详解(认证用户)

然后你开开心心的输入了用户名user,密码user,就出现了如下的界面:

2 Spring Security详解(认证用户)

403什么异常?这是SpringSecurity中的权限不足!这个异常怎么来的?还记得上面SpringSecurity内置认证页面源码中的那个_csrf隐藏input吗?问题就在这了!

1.2 CSRF防护机制

1.2.1 CSRF攻击

定义:跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。简单来讲,如果一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击,这可能会带来消极的后果。

场景:Tom登录银行网站没有退出,Jerry通过Sns获取了Tom的登录信息,向银行网站发送伪造的请求。

2 Spring Security详解(认证用户)

1.2.2 CSRF防护

CSRF防护有两种方法:

方式一:直接禁用csrf,不推荐。
方式二:在认证页面携带token请求。

(1)禁用csrf防护机制

在SpringSecurity主配置文件中添加禁用crsf防护的配置,禁用CSRF防护功能。通常来讲并不是一个好主意。如果这样做的话,那么应用就会面临CSRF攻击的风险。

       <!--去掉csrf拦截的过滤器-->
        <!--<security:csrf disabled="true"/>-->

(2)在认证页面携带token请求

Spring Security通过一个同步token的方式来实现CSRF防护的功能。它将会拦截状态变化的请求(例如,非GET、HEAD、OPTIONS和TRACE的请求)并检查CSRF token。如果请求中不包含CSRF token的话,或者token不能与服务器端的token相匹配,请求将会失败,并抛出CsrfException异常。

这意味着在你的应用中,所有的表单必须在一个“_csrf”域中提交token,而且这个token必须要与服务器端计算并存储的token一致,这样的话当表单提交的时候,才能进行匹配。

好消息是,Spring Security已经简化了将token放到请求的属性中这一任务,在JSP中创建一个“_csrf”隐藏域就可以实现CSRF防护:

	<security:csrfInput/>

这个就相当于

<input  type="hidden">
        name="${_csrf.parameterName}"
        value="${_csrf.token}"/>

注:HttpSessionCsrfTokenRepository对象负责生成token并放入session域中。

1.3 注销登录

<form action="${pageContext.request.contextPath}/logout" method="post">
      <security:csrfInput/>
      <input type="submit" value="注销">
</form>

2 查询用户详细信息

查询用户的信息,判断用户当前的用户名和密码是否合法,并且查看当前用户拥有的角色(授权)。

好消息是,Spring Security非常灵活,能够基于各种数据存储来认证用户。它内置了多种常见的用户存储场景,如内存、关系型数据库以及LDAP。我们通常使用数据库进行用户数据的存储。

2.1 使用数据库中的数据实现认证操作

查看源码,实现用自己数据库中的数据来认证操作。

通过查看源码可以得知,我们可以直接编写一个UserDetailsService的实现类,告诉SpringSecurity我们要使用数据库中的数据认证用户。

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            //UserDetails就是SpringSecurity自己的用户对象。
            //this.getUserDetailsService()其实就是得到UserDetailsService的一个实现类
            //loadUserByUsername里面就是真正的认证逻辑
            //也就是说我们可以直接编写一个UserDetailsService的实现类,告诉SpringSecurity就可以了!
            //loadUserByUsername方法中只需要返回一个UserDetails对象即可
            
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }

2.2.1 UserService接口继承UserDetailsService

public interface UserService extends UserDetailsService {

    public void save(SysUser user);

    public List<SysUser> findAll();

    public Map<String, Object> toAddRolePage(Integer id);

    public void addRoleToUser(Integer userId, Integer[] ids);
}

2.2.2 编写loadUserByUsername业务

  • 根据用户名查询用户的密码和角色。
  • 将角色(授权)保存到authorities(一个用户可以拥有多个角色)。
  • 创建UserDetails对象。
 /**
     * 认证业务
     *
     * @param username 用户在浏览器输入的用户名
     * @return UserDetails 是springsecurity自己的用户对象
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            //根据用户名做查询
            SysUser sysUser = userDao.findByName(username);
            if (sysUser == null) {
                return null;
            }
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            List<SysRole> roles = sysUser.getRoles();
            for (SysRole role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            }
            //{noop}后面的密码,springsecurity会认为是原文。
            UserDetails userDetails = new User(sysUser.getUsername(), "{noop}"+sysUser.getPassword(), authorities);
            return userDetails;
        } catch (Exception e) {
            e.printStackTrace();
            //认证失败!
            return null;
        }

    }

2.2 使用数据库中的数据实现认证操作

看一下上面的认证查询,它会预期用户密码存储在了数据库之中。这里唯一的问题在于如果密码明文存储的话,会很容易受到黑客的窃取。但是,如果数据库中的密码进行了转码的话,那么认证就会失败,因为它与用户提交的明文密码并不匹配。

为了解决这个问题,我们需要借助passwordEncoder()方法指定一个密码转码器(encoder)。Spring Security的加密模块包括了三个这样的实现 :BCryptPasswordEncoder、NoOpPasswordEncoder和StandardPasswordEncoder。内置的是StandardPasswordEncoder。

不管你使用哪一个密码转码器,都需要理解的一点是,数据库中的密码是永远不会解码的。所采取的策略与之相反,用户在登录时输入的密码会按照相同的算法进行转码,然后再与数   据库中已经转码过的密码进行对比。这个对比是在PasswordEncoder的matches()方法中进行的。

2.2.1 在IOC容器中提供密码转换器

<!--把密码转换器对象放入的IOC容器中-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

<security:authentication-manager>
        <security:authentication-provider user-service-ref="userServiceImpl">
            <security:password-encoder ref="passwordEncoder"/>
        </security:authentication-provider>
</security:authentication-manager>

2.2.2 修改认证方法

去掉{noop}

  @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        try {
            //根据用户名做查询
            SysUser sysUser = userDao.findByName(username);
            if (sysUser == null) {
                return null;
            }
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            List<SysRole> roles = sysUser.getRoles();
            for (SysRole role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            }
            //{noop}后面的密码,springsecurity会认为是原文。
            UserDetails userDetails = new User(sysUser.getUsername(),sysUser.getPassword(), authorities);
            return userDetails;
        } catch (Exception e) {
            e.printStackTrace();
            //认证失败!
            return null;
        }
    }

2.2.3 手动将数据库中用户密码改为加密后的密文

public class Encode {
    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("123");
        System.out.println(encode);
    }
}

2 Spring Security详解(认证用户)

3 remember me 功能 

2 Spring Security详解(认证用户)

对于应用程序来讲,能够对用户进行认证是非常重要的。但是站在用户的角度来讲,如果应   用程序不用每次都提示他们登录是更好的。这就是为什么许多站点提供了Remember-me功  能,你只要登录过一次,应用就会记住你,当再次回到应用的时候你就不需要登录了。

默认情况下,remember me功能是通过在cookie中存储一个token完成的,这个token最多两周内有效。存储在cookie中的token包含用户名、密码、过期时间和一个私钥——在写入cookie前都进行了MD5哈希。默认情况下,私钥的名为SpringSecured。

为了实现这一点,登录请求必须包含一个名为remember-me的参 数。在登录表单中,增加一个简单复选框就可以完成这件事情:

2 Spring Security详解(认证用户)

3.1 记住我功能原理分析

现在继续跟踪找到AbstractRememberMeServices对象的loginSuccess方法:

  • 判断是否勾选记住我
  • 若勾选就调用onLoginSuccess方法
 public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }

如果上面方法返回true,就表示页面勾选了记住我选项了。
继续顺着调用的方法找到PersistentTokenBasedRememberMeServices的onLoginSuccess方法:

  • 创建token
  • 持久化token 
   protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            this.tokenRepository.createNewToken(persistentToken);
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }

autoLogin():判断cookie是否存在,如果存在则自动登录

 public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
        String rememberMeCookie = this.extractRememberMeCookie(request);
        if (rememberMeCookie == null) {
            return null;
        } else {
            this.logger.debug("Remember-me cookie detected");
            if (rememberMeCookie.length() == 0) {
                this.logger.debug("Cookie was empty");
                this.cancelCookie(request, response);
                return null;
            } else {
                UserDetails user = null;

                try {
                    String[] cookieTokens = this.decodeCookie(rememberMeCookie);
                    user = this.processAutoLoginCookie(cookieTokens, request, response);
                    this.userDetailsChecker.check(user);
                    this.logger.debug("Remember-me cookie accepted");
                    return this.createSuccessfulAuthentication(request, user);
                } catch (CookieTheftException var6) {
                    this.cancelCookie(request, response);
                    throw var6;
                } catch (UsernameNotFoundException var7) {
                    this.logger.debug("Remember-me login was valid but corresponding user not found.", var7);
                } catch (InvalidCookieException var8) {
                    this.logger.debug("Invalid remember-me cookie: " + var8.getMessage());
                } catch (AccountStatusException var9) {
                    this.logger.debug("Invalid UserDetails: " + var9.getMessage());
                } catch (RememberMeAuthenticationException var10) {
                    this.logger.debug(var10.getMessage());
                }

                this.cancelCookie(request, response);
                return null;
            }
        }
    }

 3.2 记住我功能页面代码

 注意name和value属性的值不要写错哦!

<div class="checkbox icheck">
<label><input type="checkbox" name="remember-me" value="true"> 记住 下次自动登录</label>
</div>

3.3 开启remember me过滤器 

 <security:http auto-config="true" use-expressions="true">
     <security:remember-me token-validity-seconds="60"/>
 </security:http>

说明:RememberMeAuthenticationFilter中功能非常简单,会在打开浏览器时,自动判断是否认证,如果没有则调用autoLogin进行自动认证。 

3.4 remember me安全性分析

记住我功能方便是大家看得见的,但是安全性却令人担忧。因为Cookie毕竟是保存在客户端的,很容易盗取,而且cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全(如下图所示)。那么这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。

2 Spring Security详解(认证用户)

此外,SpringSecurity还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在db中保存该加密串-用户信息的对应关系,自动登录时,用cookie中的加密串,到db中验证,如果通过,自动登录才算通过。

 

相关标签: Spring实战