认证用户的过程:进入认证页面-->输入用户名和密码-->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,密码user,就出现了如下的界面:

2 Spring Security详解(认证用户)_User_02

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详解(认证用户)_用户名_03

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详解(认证用户)_用户名_04

3 remember me 功能 

2 Spring Security详解(认证用户)_User_05

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

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

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

2 Spring Security详解(认证用户)_数据库_06

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详解(认证用户)_数据库_07

此外,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"/>

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

2 Spring Security详解(认证用户)_用户名_08

4 显示当前认证用户名

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

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

2 Spring Security详解(认证用户)_User_09