2 Spring Security详解(认证用户)

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

项目的源码:https://download.csdn.net/download/A1342772/12132301

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>

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

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

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的登录信息,向银行网站发送伪造的请求。

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

3 remember me 功能 

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

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

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

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的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全(如下图所示)。那么这就要提醒喜欢使用此功能的,用完网站要及时手动退出登录,清空认证信息。

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

创建一张表,注意这张表的名称和字段都是固定的,不要修改。

CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

然后将spring-security.xml中 改为:

<!-- 
    开启remember me过滤器,
    data-source-ref="dataSource" 指定数据库连接池
    token-validity-seconds="60" 设置token存储时间为60秒 可省略
    remember-me-parameter="remember-me" 指定记住的参数名 可省略
--> 
<security:remember-me
       data-source-ref="dataSource"
       token-validity-seconds="60"
       remember-me-parameter="remember-me"/>

最后测试发现数据库中自动多了一条记录:

4 显示当前认证用户名

在header.jsp中找到页面头部最右侧图片处添加如下信息:

<span class="hidden-xs">
    <%--<security:authentication property="principal.username" />--%>
    <security:authentication property="name" />
</span>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值