1 一个简单的security项目
1.1 依赖
<!--jsp页面的el标签-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!--security的配置包-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
1.2 spring security 几个包
spring-security-core.jar
核心包,任何Spring Security功能都需要此包。
spring-security-web.jar
web工程必备,包含过滤器和相关的Web安全基础结构代码。
spring-security-confifig.jar
用于解析xml配置文件,用到Spring Security的xml配置文件的就要用到此包。
spring-security-taglibs.jar
Spring Security提供的动态标签库,jsp页面可以用。
1.3 配置web.xml
<!--security的过滤器链,名字必须固定为springSecurityFilterChain-->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
1.4 配置spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--
auto-config="true" 使用security的默认配置
use-expressions="true" 使用jsp页面security的el表达式
-->
<security:http auto-config="true" use-expressions="true">
<!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
</security:http>
<!--配置一个用户,这里是模拟,不连数据库-->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<!--
password="{noop}1234" {noop}表示不使用密码加密功能
-->
<security:user name="lx" authorities="ROLE_USER" password="{noop}1234"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
1.5 使spring能读取到spring-security.xml
在applicationContext.xml(spring 的核心配置文件)配置文件中加入
<!--导入springsecurity的配置文件-->
<import resource="spring-security.xml"></import>
1.6 运行项目
此页面是spring security中自带的页面
输入账号密码后会成功登录到首页
2 security的过滤器链
2.1 流程
上面程序启动的时候,会在控制台打印图片内容,你会发现,security加载了一系列的过滤器。
查看源码org.springframework.web.filter.DelegatingFilterProxy
,也就是web.xml中的过滤器链
直接找doFilter方法,里面所有判断通过后,会调用一个方法initDelegate
debug到最后一步,你会发现返回一个FilterChainProxy的过滤器
进入该类的源码
进入doFilterInternal方法
再来看看第二步中的getFilters方法
最后看SecurityFilterChain,这是个接口,实现类也只有一个DefaultSecurityFilterChain
,这才是web.xml中配置的过滤器链对象,也就是控制台上面打印的那个。
2.2 security中的过滤器
访问资源的时候,会经过一个个的过滤器,每个过滤器有自己的功能,只有所有过滤器都验证通过后才能访问到资源
- org.springframework.security.web.context.SecurityContextPersistenceFilter
首当其冲的一个过滤器,作用之重要,自不必多言。
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个
SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
SecurityContext中存储了当前用户的认证以及权限信息。
- org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
- org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
- org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
如果不包含,则报错。起到防止csrf攻击的效果。
- org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
- org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
- org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
- org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
由此过滤器可以生产一个默认的退出登录页面
- org.springframework.security.web.authentication.www.BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
- org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
- org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
- org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
- org.springframework.security.web.session.SessionManagementFilter
SecurityContextRepository限制同一用户开启多个会话的数量
- org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
- org.springframework.security.web.access.intercept.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权
限
3 security中使用自定义的登录认证页面
3.1 修改spring-security.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--静态资源无需经过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"/>
<!--
auto-config="true" 使用security的默认配置
use-expressions="true" 使用jsp页面security的el表达式
-->
<security:http auto-config="true" use-expressions="true">
<!--login.jsp允许匿名访问-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
<!--
login-page="/login.jsp" 指定自定义的登录页面
login-processing-url="/login"
default-target-url="/index.jsp" 指定登录成功后的默认页面
authentication-failure-url="/failer.jsp" 指定未登录访问资源跳转的页面
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp" />
<!--
logout-url="/logout" 指定处理登出的controller路径
logout-success-url="/login.jsp" 指定登出成功后跳转的路径
-->
<security:logout logout-url="/logout" logout-success-url="/login.jsp"/>
<!--禁止开启跨站请求伪造,如果不开启此功能,登录后访问首页仍然会报403错误-->
<security:csrf disabled="true"/>
</security:http>
<!--内存中配置一个用户-->
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<!--
password="{noop}1234" {noop}表示不使用密码加密功能
-->
<security:user name="lx" authorities="ROLE_USER" password="{noop}1234"></security:user>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
3.2 修改login.jsp
处理登录过程的地址是/login
<div class="login-box-body">
<p class="login-box-msg">登录系统</p>
<form action="${pageContext.request.contextPath}/login" method="post">
<div class="form-group has-feedback">
<input type="text" name="username" class="form-control"
placeholder="用户名"> <span
class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" name="password" class="form-control"
placeholder="密码"> <span
class="glyphicon glyphicon-lock form-control-feedback"></span>
</div>
<div class="row">
<div class="col-xs-8">
<div class="checkbox icheck">
<label><input type="checkbox" name="remember-me" value="true"> 记住 下次自动登录</label>
</div>
</div>
<!-- /.col -->
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">登录</button>
</div>
<!-- /.col -->
</div>
</form>
<a href="#">忘记密码</a><br>
</div>
3.3 修改登出的页面代码
<!-- Menu Footer-->
<li class="user-footer">
<div class="pull-left">
<a href="#" class="btn btn-default btn-flat">修改密码</a>
</div>
<div class="pull-right">
<%--security规定注销必须使用post请求--%>
<form action="${pageContext.request.contextPath}/logout" method="post">
<button type="submit" class="btn btn-default btn-flat">注销</button>
</form>
</div>
</li>
4 security中csrf防护机制
如果我们没有此项配置,登录后访问首页仍会报403(权限不足)
4.1 关闭csrf
只需在spring-security.xml中加入这项配置即可
<security:csrf disabled="true"/>
4.2 在自定义的登录页面中使用csrf
如果我们要使用这个csrf的话,我们必须在登录的jsp中加入如下内容
<%--使用security的el标签库--%>
<%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
在登录的form表单中加入下面内容
<%--会自动产生一个csrf参数--%>
<security:csrfInput/>
启动项目访问登录页面,F12查看源代码
输入账号密码,可以正常登录了,但是点注销按钮的时候,发现又报了403,这是什么原因呢?
4.2.1 csrf机制的流程
csrf是由security过滤器链中的CsrfFilter提供的,我们进去看一下源码
这个matches方法到底是干什么的,我们接着找
我们还发现DefaultRequiresCsrfMatcher他是一个内部类
如果不是这四个方法类型,我们看doFilterInternal方法开头
它将csrfToken设置到request上,就是为了我们能在页面上使用security的el标签获取到token,最后在表单提交的时候,将这个token作为请求体中的一个参数发送给服务器
所以最后的结论就是:它会判断方法的类型,如果是"GET", “HEAD”, “TRACE”, "OPTIONS"之一,直接放行,否则的话,就会匹配表单中的actualToken和和服务器上的csrfToken是否相等,相等就放行,否则在控制台打印Invalid CSRF token found for ....
,这样如果别的用户没有拿到对应的csrfToken就不能对系统做任何修改删除操作
5 连接数据库
5.1 取得UserDetails(封装了用户信息)
根据上面的内容我们已经知道了UsernamePasswordAuthenticationFilter这个过滤器是处理认证过程的,我们现在来分析一下
由此可知,真正的认证过程在AuthenticationManager的对象中,找这个接口的实现类
进去看下源码,注意一下这个providers
认证方法
我们接着去找AuthenticationProvider的实现类DaoAuthenticationProvider,authenticate方法在AbstractUserDetailsAuthenticationProvider中
DaoAuthenticationProvider重写了retrieveUser方法,现在我们去DaoAuthenticationProvider类
UserDetails就是SpringSecurity自己的用户对象。
this.getUserDetailsService()其实就是得到UserDetailsService的一个实现类
loadUserByUsername里面就是真正的认证逻辑
也就是说我们可以直接编写一个UserDetailsService的实现类,告诉SpringSecurity就可以了!
loadUserByUsername方法中只需要返回一个UserDetails对象即可
接下来就简单了,我们只需实现这个接口就可以获取我们自己数据库中的用户信息
5.2 取得UserDetails之后
在AbstractUserDetailsAuthenticationProvider中
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//最后一行,看看对他做了啥
return createSuccessAuthentication(principalToReturn, authentication, user);
}
我们去UsernamePasswordAuthenticationToken的构造方法看看
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
//主要看这里,我们看他对权限做了啥
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
进入父类的构造方法
/**
* Creates a token with the supplied array of authorities.
*
* @param authorities the collection of <tt>GrantedAuthority</tt>s for the principal
* represented by this authentication object.
*/
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
//权限不能为空
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
return;
}
//权限不能为空
for (GrantedAuthority a : authorities) {
if (a == null) {
throw new IllegalArgumentException(
"Authorities collection cannot contain any null elements");
}
}
//把权限重新放到list集合中
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);
}
由此,我们需要牢记自定义认证业务逻辑返回的UserDetails对象中一定要放置权限信息
5.3 认证成功后做的操作
现在我们回到UsernamePasswordAuthenticationFilter这个过滤器,我们一直没有找到他的doFilter方法,现在,我们去他父类AbstractAuthenticationProcessingFilter找找。
/**
* Invokes the
* {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)
* requiresAuthentication} method to determine whether the request is for
* authentication and should be handled by this filter. If it is an authentication
* request, the
* {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
* attemptAuthentication} will be invoked to perform the authentication. There are
* then three possible outcomes:
* <ol>
* <li>An <tt>Authentication</tt> object is returned. The configured
* {@link SessionAuthenticationStrategy} will be invoked (to handle any
* session-related behaviour such as creating a new session to protect against
* session-fixation attacks) followed by the invocation of
* {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
* method</li>
* <li>An <tt>AuthenticationException</tt> occurs during authentication. The
* {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
* unsuccessfulAuthentication} method will be invoked</li>
* <li>Null is returned, indicating that the authentication process is incomplete. The
* method will then return immediately, assuming that the subclass has done any
* necessary work (such as redirects) to continue the authentication process. The
* assumption is that a later request will be received by this method where the
* returned <tt>Authentication</tt> object is not null.
* </ol>
*/
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//这里就是判断请求是否是登录认证(/login)请求,如果是其他请求就直接放行
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
//登录失败去了这里,一会我们去看看
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//登录成功去这里
successfulAuthentication(request, response, chain, authResult);
}
先看登录成功后的操作
/**
* Default behaviour for successful authentication.
* <ol>
* <li>Sets the successful <tt>Authentication</tt> object on the
* {@link SecurityContextHolder}</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* Subclasses can override this method to continue the {@link FilterChain} after
* successful authentication.
* @param request
* @param response
* @param chain
* @param authResult the object returned from the <tt>attemptAuthentication</tt>
* method.
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//认证成功,将认证信息放入securityContext容器中
SecurityContextHolder.getContext().setAuthentication(authResult);
//认证成功,使用记住我功能
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
登录失败后的操作
/**
* Default behaviour for unsuccessful authentication.
* <ol>
* <li>Clears the {@link SecurityContextHolder}</li>
* <li>Stores the exception in the session (if it exists or
* <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
* <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li>
* </ol>
*/
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
//记住我 失败
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
可见AbstractAuthenticationProcessingFilter这个过滤器对于认证成功与否,做了两个分支,成功执行
successfulAuthentication,失败执行unsuccessfulAuthentication。
在successfulAuthentication内部,将认证信息存储到了SecurityContext中。并调用了loginSuccess方法,这就是
常见的“记住我”功能!
5.4 初步实现认证功能
5.4.1 使自己的UserService接口继承UserDetailsService
//继承接口
public interface UserService extends UserDetailsService {
}
5.4.2 实现loadUserByUsername方法
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名获取用户对象
SysUser sysUser = userDao.findByName(username);
if (null == sysUser) {
//不存在直接返回空,security会自动对它处理
return null;
}
/**
* UserDetails是一个抽象类,我们找它的实现类User,它有两个构造方法,我们使用简单的那一个
* username 用户名
* password 密码
* Collection<? extends GrantedAuthority> authorities 集合,里面的元素为GrantedAuthority的实现类,这里我们用SimpleGrantedAuthority
*/
List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
for (SysRole role : sysUser.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
UserDetails userDetails = new User(sysUser.getUsername(),
"{noop}" + sysUser.getPassword(),
authorities);
return userDetails;
}
}
5.4.3 配置spring-security.xml使自己重写的loadUserByUsername生效
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<!--
user-service-ref="userServiceImpl" 指定使用这个对象去数据库加载用户信息
对象名字就是类名首字母小写
-->
<security:authentication-provider user-service-ref="userServiceImpl"></security:authentication-provider>
</security:authentication-manager>
5.4.4 测试
测试的时候必须给测试的用户配置角色,否则仍然会登录失败
5.5 使用加密认证
5.5.1 修改spring-security.xml配置文件
<!--加密对象-->
<bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<!--
user-service-ref="userServiceImpl" 指定使用这个对象去数据库加载用户信息
对象名字就是类名首字母小写
-->
<security:authentication-provider user-service-ref="userServiceImpl">
<security:password-encoder ref="bCryptPasswordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
5.5.2 修改loadUserByUsername方法,去掉“{noop}”
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userDao.findByName(username);
if (null == sysUser) {
return null;
}
/**
* username 用户名
* password 密码
* Collection<? extends GrantedAuthority> authorities 集合,里面的元素为GrantedAuthority的实现类,这里我们用SimpleGrantedAuthority
*/
List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
for (SysRole role : sysUser.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
UserDetails userDetails = new User(sysUser.getUsername(),
//去掉“{noop}”
sysUser.getPassword(),
authorities);
return userDetails;
}
5.5.3 测试
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("123"));
}
直接修改数据库
5.6 使用用户状态
我们在loadUserByUsername方法中构造UserDetails的对象的时候,使用的是三个参数的User构造方法,其实他还有一个构造方法,这个构造方法中就加入了用户状态
5.6.1 User的第二个构造方法
/**
* Construct the <code>User</code> with the details required by
* {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider}.
*
* @param username the username presented to the
* <code>DaoAuthenticationProvider</code>
* @param password the password that should be presented to the
* <code>DaoAuthenticationProvider</code>
* @param enabled set to <code>true</code> if the user is enabled
* @param accountNonExpired set to <code>true</code> if the account has not expired
* @param credentialsNonExpired set to <code>true</code> if the credentials have not
* expired
* @param accountNonLocked set to <code>true</code> if the account is not locked
* @param authorities the authorities that should be granted to the caller if they
* presented the correct username and password and the user is enabled. Not null.
*
* @throws IllegalArgumentException if a <code>null</code> value was passed either as
* a parameter or as an element in the <code>GrantedAuthority</code> collection
*/
public User(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (((username == null) || "".equals(username)) || (password == null)) {
throw new IllegalArgumentException(
"Cannot pass null or empty values to constructor");
}
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
下面是几个布尔参数的解释,也可以看方法上的注释,还是很简单的
boolean enabled 是否可用
boolean accountNonExpired 账户是否失效
boolean credentialsNonExpired 密码是否失效
boolean accountNonLocked 账户是否锁定
5.6.2 判断用户的认证状态
UserDetails userDetails = new User(sysUser.getUsername(),
sysUser.getPassword(),
//根据业务的需要选择使用,这里我们只使用一个
//status的值为1的时候,启用该账号
sysUser.getStatus()==1,
true,
true,
true,
authorities);
6 remember me
6.1 记住我功能原理分析
从AbstractAuthenticationProcessingFilter类中找到doFilter方法,我们发现认证成功后走的方法是
/**
* Default behaviour for successful authentication.
* <ol>
* <li>Sets the successful <tt>Authentication</tt> object on the
* {@link SecurityContextHolder}</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* Subclasses can override this method to continue the {@link FilterChain} after
* successful authentication.
* @param request
* @param response
* @param chain
* @param authResult the object returned from the <tt>attemptAuthentication</tt>
* method.
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
//remember me功能的代码
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
我们发现rememberMeServices是NullRememberMeServices的实例对象
private RememberMeServices rememberMeServices = new NullRememberMeServices();
进入loginSuccess这个方法看看,发现方法为空。我们使用debug打断点
发现确实是NullRememberMeServices的实例对象,为什么会这样,原来是我们在登录的时候没有使用记住我的这个功能,现在我我们开启这个功能
6.2 使用remember me
6.2.1 修改登录的表单
所以我们现在去修改页面,开启remember me
<%--登录表单中添加这个--%>
<input type="checkbox" name="remember-me" value="true">
6.2.2 修改spring-security.xml
security默认是不开启remember me这个功能的
<!--
auto-config="true" 使用security的默认配置
use-expressions="true" 使用jsp页面security的el表达式
-->
<security:http auto-config="true" use-expressions="true">
<!--login.jsp允许匿名访问-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--使用spring的el表达式来指定项目所有资源访问都必须有ROLE_USER或ROLE_ADMIN角色-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>
<!--
login-page="/login.jsp" 指定自定义的登录页面
login-processing-url="/login"
default-target-url="/index.jsp" 指定登录成功后的默认页面
authentication-failure-url="/failer.jsp" 指定未登录访问资源跳转的页面
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp" />
<!--
logout-url="/logout" 指定处理登出的controller路径
logout-success-url="/login.jsp" 指定登出成功后跳转的路径
-->
<security:logout logout-url="/logout" logout-success-url="/login.jsp"/>
<!--禁止开启跨站请求伪造,如果不开启此功能,登录后访问首页仍然会报403错误-->
<security:csrf disabled="true"/>
<!--
开启remember me过滤器
token-validity-seconds="60" 设置token存储时间为60秒
-->
<security:remember-me token-validity-seconds="60"/>
</security:http>
RememberMeAuthenticationFilter中功能非常简单,会在打开浏览器时,自动判断是否认证,如果没有则
调用autoLogin进行自动认证
6.2.3 测试
先测试一下,认证通过后,关掉浏览器,再次打开页面,不需要认证了!
现在我们接着去debug,看一下rememberMeServices的对象到底是什么
好了,现在我们知道remember me这个功能主要由这个类完成
6.3 接着说原理
进入TokenBasedRememberMeServices的loginSuccess方法(在父类中)
/**
* {@inheritDoc}
*
* <p>
* Examines the incoming request and checks for the presence of the configured
* "remember me" parameter. If it's present, or if <tt>alwaysRemember</tt> is set to
* true, calls <tt>onLoginSucces</tt>.
* </p>
*/
@Override
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
//这里判断是否勾选记住我
//这里this.parameter点进去是上面的private String parameter = "remember-me";
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
//若勾选就调用onLoginSuccess方法
onLoginSuccess(request, response, successfulAuthentication);
}
再去看看这个rememberMeRequested,方法很简单
/**
* Allows customization of whether a remember-me login has been requested. The default
* is to return true if <tt>alwaysRemember</tt> is set or the configured parameter
* name has been included in the request and is set to the value "true".
*
* @param request the request submitted from an interactive login, which may include
* additional information indicating that a persistent login is desired.
* @param parameter the configured remember-me parameter name.
*
* @return true if the request includes information indicating that a persistent login
* has been requested.
*/
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (alwaysRemember) {
return true;
}
//从request请求中获取remember-me的值
String paramValue = request.getParameter(parameter);
//若要开启这个功能,其对应值必须是true,on,yes,1
if (paramValue != null) {
if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
|| paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
return true;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Did not send remember-me cookie (principal did not set parameter '"
+ parameter + "')");
}
return false;
}
如果上面方法返回true,就表示页面勾选了记住我选项了。
再去看看这个loginSuccess方法最后一行调用的onLoginSuccess的方法(TokenBasedRememberMeServices中)
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// If unable to find a username and password, just abort as
// TokenBasedRememberMeServices is
// unable to construct a valid token in this case.
if (!StringUtils.hasLength(username)) {
logger.debug("Unable to retrieve username");
return;
}
if (!StringUtils.hasLength(password)) {
//从数据库中获取用户信息
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
logger.debug("Unable to obtain password for user: " + username);
return;
}
}
//获取token最大存活时间,前面在配置文件中设置的60
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
//当前系统时间(毫秒数)
long expiryTime = System.currentTimeMillis();
// SEC-949
//到期时间
expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
//生成签名(签名中包含了用户名和密码信息)
String signatureValue = makeTokenSignature(expiryTime, username, password);
//根据签名生成了cookie,并返回
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
tokenLifetime, request, response);
if (logger.isDebugEnabled()) {
logger.debug("Added remember-me cookie for user '" + username
+ "', expiry: '" + new Date(expiryTime) + "'");
}
}
在浏览器中会有一个名字为remember-me的cookie
原理到这里就说的差不多了
6.4 持久化remember me信息
记住我功能是很方便,但是安全性却令人担忧。因为Cookie毕竟是保存在客户端的,很容易盗取,而且 cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。于是,SpringSecurity就提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录 时,用cookie中的加密串,到数据库中验证,如果通过,自动登录才算通过。
6.4.1 建表
这个表示固定的,不要修改它的结构
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
6.4.2 修改spring-security.xml
<security:http auto-config="true" use-expressions="true">
<!--省略上面的内容-->
<!--
开启remember me过滤器
token-validity-seconds="60" 设置token存储时间为60秒
data-source-ref="dataSource" 指定数据源,因为需要将用户信息保存到数据库
remember-me-parameter="remember-me" 指定记住的参数名
-->
<security:remember-me token-validity-seconds="60" data-source-ref="dataSource" remember-me-parameter="remember-me" />
</security:http>
6.4.3 测试
勾选记住我登录之后
数据库中
浏览器中
6.4.4 原理
debug模式下,仍然对rememberMeServices.loginSuccess(request, response, authResult);
这行打断点,然后勾选记住我登录。
我们发现这个rememberMeServices又换了一种类型,好,我们接着按照6.3的过一遍
那我们进入PersistentTokenBasedRememberMeServices的loginSuccess方法(在父类AbstractRememberMeServices中,这两父类一样,那loginSuccess方法也就是一样的罗,rememberMeRequested方法也是一样的,唯一区别就是onLoginSuccess方法了)
我们看一下PersistentTokenBasedRememberMeServices的onLoginSuccess方法是咋样的
/**
* Creates a new persistent login token with a new series number, stores the data in
* the persistent token repository and adds the corresponding cookie to the response.
*
*/
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
//持久化到数据库
tokenRepository.createNewToken(persistentToken);
//生成对应的cookie
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
7 权限控制
SpringSecurity可以通过注解的方式来控制类或者方法的访问权限。注解需要对应的注解支持,若注解放在 controller类中,对应注解支持应该放在mvc配置文件中,因为controller类由mvc配置文件扫描并创建的,同理,注解放在service类中,对应注解支持应该放在spring配置文件中。
7.1 注解支持放在mvc的配置文件中
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<context:component-scan base-package="com.itheima.controller"/>
<mvc:annotation-driven/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
<mvc:default-servlet-handler/>
<!--
jsr250-annotations="enabled" java自带的 表示支持jsr250-api的注解,需要jsr250-api的jar包
pre-post-annotations="enabled" spring带的注解
secured-annotations="enabled" security带的注解
都是权限相关的注解,我们只需要使用其中一种就可以了,这里我们都开启
-->
<security:global-method-security jsr250-annotations="enabled" secured-annotations="enabled" pre-post-annotations="enabled"/>
</beans>
7.2 三类注解
@RolesAllowed({"ROLE_ADMIN","ROLE_PRODUCT"})//JSR-250注解
@PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_PRODUCT')")//spring表达式注解
@Secured({"ROLE_ADMIN","ROLE_PRODUCT"})//SpringSecurity注解
8 权限不足异常处理
系统的403页面太难看了,对用户不友好,我们对它做下美化
8.1 编写一个异常处理器
@ControllerAdvice
public class ExceptionAdvice {
//403异常就跳转到这个页面
@ExceptionHandler(AccessDeniedException.class)
public String handleAccessDeniedException(){
return "forward:/403.jsp";
}
}
这个类必须要放到controller中,只有这样注解才能被mvc扫描到