1、权限管理的概念
权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管
理几乎出现在任何系统里面,前提是需要有用户和密码认证的系统。
在权限管理的概念中,有两个非常重要的名词:
-
认证:通过用户名和密码成功登陆系统后,让系统得到当前用户的角色身份。
登录和认证的区别:一般简单的登录是只要验证用户的用户名和密码是否正确,但是认证还包括查询出用户的角色和权限信息。
-
授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。
权限管理中三个重要的对象:
-
用户:主要包含用户名,密码和当前用户的角色信息,可实现认证操作。
-
角色:主要包含角色名称,角色描述和当前角色拥有的权限信息,可实现授权操作。
-
权限:权限也可以称为菜单,主要包含当前权限名称,url地址等信息,可实现动态展示菜单。
注:这三个对象中,用户与角色是多对多的关系,角色与权限是多对多的关系,用户与权限没有直接关系,
二者是通过角色来建立关联关系的。
2、Spring Security入门
初始化的工程使用的框架是Spring SpringMVC和MyBatis,数据库是mysql。
数据库脚本:
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`permission_NAME` varchar(30) DEFAULT NULL COMMENT '菜单名称',
`permission_url` varchar(100) DEFAULT NULL COMMENT '菜单地址',
`parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父菜单id',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`ROLE_NAME` varchar(30) DEFAULT NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`RID` int(11) NOT NULL COMMENT '角色编号',
`PID` int(11) NOT NULL COMMENT '权限编号',
PRIMARY KEY (`RID`,`PID`),
KEY `FK_Reference_12` (`PID`),
CONSTRAINT `FK_Reference_11` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
CONSTRAINT `FK_Reference_12` FOREIGN KEY (`PID`) REFERENCES `sys_permission` (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL COMMENT '用户名称',
`password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`status` int(1) DEFAULT '1' COMMENT '1开启0关闭',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`,`RID`),
KEY `FK_Reference_10` (`RID`),
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `sys_role` (`ID`),
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Spring Security是spring采用AOP思想,基于servlet过滤器实现的安全框架。它提供了完善的认证机制和方法级的授权功能,是一款非常优秀的权限管理框架。
2.1 jar包导入
Spring Security主要jar包功能介绍
-
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
页面可以用。
如果是maven项目,因为具有依赖的传递性,所以只需要导入下面的两个依赖即可:
<!--SpringSecurity相关的包-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
2.2 web.xml配置过滤器链
Spring Security是基于过滤器链来实现认证和授权的,所以需要在web.xml中配置:
<!--上面省略了很多其他的配置,比如SpringMvc的前置Servlet-->
<!--Spring 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>
2.3 编写SpringSecurity的配置文件
Spring Security需要配置很多的东西,比如用户,权限,拦截规则等,所以编写一个单独的配置文件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: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">
<!--配置springSecurity-->
<!--
auto-config="true" 表示自动加载springsecurity的配置文件
use-expressions="true" 表示使用spring的el表达式来配置springsecurity
-->
<security:http auto-config="true" use-expressions="true">
<!--拦截资源-->
<!--
pattern="/**" 表示拦截所有资源
access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
</security:http>
<!--设置Spring Security认证用户信息的来源-->
<!--
springsecurity默认的认证必须是加密的,加上{noop}表示不加密认证。
-->
<security:authentication-manager>
<!--现在在内存中配置两个用户-->
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user" authorities="ROLE_USER"/>
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
2.4 在Spring的主配置文件applicationContext.xml中引入spring-security.xml
因为项目启动的时候,只会去加载web.xml,而我们在web.xml中配置了SpringMvc的前置servlet和Spring的ContextLoaderListener,里面指定了相关的配置文件路径,所以如果想要项目启动可以加载spring-security.xml,按理也是需要在web.xml中进行配置的,我们在applicationContext.xml中引入也是达到一样的效果。
<!--引入SpringSecurity配置文件,才会被Spring容器加载-->
<import resource="classpath:spring-security.xml"/>
这里可能有人存在一个疑问:到底应该在把spring-security的相关配置交给Spring管理还是SpringMVC管理?
Spring的IOC容器是父容器,通过Http的方式是不能被访问的,SpringMVC的容器是子容器,Http访问的就是子容器中的Controller,子容器可以访问父容器的Bean,而父容器不能访问子容器。这也就是我们经常在Controller中注入Service的原因。而我们的Spring Secutiry的配置,肯定是不需要让外界知道的,所以我们应该把他交给Spring的IOC容器管理。
2.5 启动项目测试
2.5.1 为啥到了不是我们写的登录页面
项目启动需要进行tomcat的配置,相关的配置请自行百度;
项目启动完成,会自动访问:http://localhost:8080/index.jsp,但是我们看到给我们弹出的页面是:
为什么会这样?
因为我们配置Spring Security的拦截所有的路径:
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
因为当前还没登录,所以就会跳转到login登录页面,但是这个登录页面,以及这个跳转操作,都不是我们实现的,而是Spring Security内部帮我们实现的。如果是早期的版本,这个登录页面是没样式的,很丑。
2.5.2 CSRF
F12查看这个页面的原代码,会发现有这样一行:
<input name="_csrf" type="hidden" value="5f4a7ee0-1494-483b-af32-98670facbebf">
这一行很重要,注意!这可是你想使用自定义页面时,排查问题的一条重要线索!后面会讲到。
2.5.3 AccessDeniedException
此时查看后端控制台,会发现一个这样的异常:
org.springframework.security.access.AccessDeniedException: Access is denied
这是因为项目启动会直接访问index.jsp页面,该请求会拦截了,因为需要ROLE_USER才能访问,所以Spring Security 抛出了这个异常。
2.5.4 15个过滤器
仔细找的话,你还能发现控制台里面的日志里面显示加载了下面这15个过滤器:
'SecurityContextPersistenceFilter'
'WebAsyncManagerIntegrationFilter'
'HeaderWriterFilter'
'CsrfFilter'
'LogoutFilter'
'UsernamePasswordAuthenticationFilter'
'DefaultLoginPageGeneratingFilter'
'DefaultLogoutPageGeneratingFilter'
'BasicAuthenticationFilter'
'RequestCacheAwareFilter'
'SecurityContextHolderAwareRequestFilter'
'AnonymousAuthenticationFilter'
'SessionManagementFilter'
'ExceptionTranslationFilter'
'FilterSecurityInterceptor'
这些就是之前说的Spring Security的过滤器链。
2.5.5 使用user用户登录
如果想要正常的访问到首页,我们需要使用user登录,因为user用户拥有ROLE_USER角色。
3、Spring Security的过滤器链
3.1 Spring Security常用过滤器介绍
过滤器是一种典型的AOP思想,关于什么是过滤器,就不赘述了,谁还不知道凡是web工程都能用过滤器?
接下来咱们就一起看看Spring Security中这些过滤器都是干啥用的,源码我就不贴出来了,有名字,大家可以自
己在idea中Double Shift去。我也会在后续的学习过程中穿插详细解释。
-
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
中存储的用户信息来决定其是否有权限。
那么,是不是spring security一共就这么多过滤器呢?答案是否定的!随着spring-security.xml配置的添加,还会出现新的过滤器。
那么,是不是spring security每次都会加载这些过滤器呢?答案也是否定的!随着spring-security.xml配置的修改,有些过滤器可能会被去掉。
3.2 Spring Security过滤器链加载原理
通过前面十五个过滤器功能的介绍,对于SpringSecurity简单入门中的疑惑是不是在心中已经有了答案了呀?
但新的问题来了!我们并没有在web.xml中配置这些过滤器啊?它们都是怎么被加载出来的?
友情提示:前方高能预警,吃饭喝水打瞌睡的请睁大眼睛,专注心神!
3.2.1 DelegatingFilterProxy
我们在web.xml中配置了一个名称为springSecurityFilterChain的过滤器DelegatingFilterProxy,接下我直接对
DelegatingFilterProxy源码里重要代码进行说明,其中删减掉了一些不重要的代码,大家注意我写的注释就行了!
package org.springframework.web.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
public class DelegatingFilterProxy extends GenericFilterBean {
@Nullable
private String contextAttribute;
@Nullable
private WebApplicationContext webApplicationContext;
@Nullable
private String targetBeanName;
private boolean targetFilterLifecycle;
@Nullable
private volatile Filter delegate;//注:这个过滤器才是真正加载的过滤器
private final Object delegateMonitor;
//注:doFilter才是过滤器的入口,直接从这看!
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized(this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = this.findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
}
//第一步:doFilter中最重要的一步,初始化上面私有过滤器属性delegate
delegateToUse = this.initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
//第三步:执行FilterChainProxy过滤器
this.invokeDelegate(delegateToUse, request, response, filterChain);
}
//第二步:直接看最终加载的过滤器到底是谁
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}
return delegate;
}
protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
protected void destroyDelegate(Filter delegate) {
if (this.isTargetFilterLifecycle()) {
delegate.destroy();
}
}
}
第二步debug结果如下:
由此可知,DelegatingFilterProxy通过springSecurityFilterChain这个名称,得到了一个FilterChainProxy过滤器,最终在第三步执行了这个过滤器。
3.2.2 FilterChainProxy
注意代码注释!注意代码注释!注意代码注释!
package org.springframework.security.web;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.web.filter.GenericFilterBean;
public class FilterChainProxy extends GenericFilterBean {
private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
private List<SecurityFilterChain> filterChains;
private FilterChainProxy.FilterChainValidator filterChainValidator;
private HttpFirewall firewall;
//咿!?可以通过一个叫SecurityFilterChain的对象实例化出一个FilterChainProxy对象
//这FilterChainProxy又是何方神圣?会不会是真正的过滤器链对象呢?先留着这个疑问!
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
//又是SecurityFilterChain这家伙!嫌疑更大了!
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChainValidator = new FilterChainProxy.NullFilterChainValidator();
this.firewall = new StrictHttpFirewall();
this.filterChains = filterChains;
}
//注:直接从doFilter看
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//第一步:具体操作调用下面的doFilterInternal方法了
this.doFilterInternal(request, response, chain);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
} else {
//第一步:具体操作调用下面的doFilterInternal方法了
this.doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
HttpServletResponse fwResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
//第二步:封装要执行的过滤器链,那么多过滤器就在这里被封装进去了!
List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
if (filters != null && filters.size() != 0) {
FilterChainProxy.VirtualFilterChain vfc = new FilterChainProxy.VirtualFilterChain(fwRequest, chain, filters);
//第四步:加载过滤器链
vfc.doFilter(fwRequest, fwResponse);
} else {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest) + (filters == null ? " has no matching filters" : " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
}
}
//第三步:封装过滤器链到SecurityFilterChain中!
private List<Filter> getFilters(HttpServletRequest request) {
Iterator var2 = this.filterChains.iterator();
//第三步:封装过滤器链到SecurityFilterChain中!
SecurityFilterChain chain;
do {
if (!var2.hasNext()) {
return null;
}
chain = (SecurityFilterChain)var2.next();
} while(!chain.matches(request));
return chain.getFilters();
}
}
第二步debug结果如下图所示,惊不惊喜?十五个过滤器都在这里了!
再看第三步,怀疑这么久!原来这些过滤器还真是都被封装进SecurityFilterChain中了。
3.2.3 SecurityFilterChain
最后看SecurityFilterChain,这是个接口,实现类也只有一个,这才是web.xml中配置的过滤器链对象!
public interface SecurityFilterChain {
boolean matches(HttpServletRequest var1);
List<Filter> getFilters();
}
实现类:DefaultSecurityFilterChain
package org.springframework.security.web;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.util.matcher.RequestMatcher;
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private static final Log logger = LogFactory.getLog(DefaultSecurityFilterChain.class);
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
this(requestMatcher, Arrays.asList(filters));
}
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
this.requestMatcher = requestMatcher;
this.filters = new ArrayList(filters);
}
public RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
public List<Filter> getFilters() {
return this.filters;
}
public boolean matches(HttpServletRequest request) {
return this.requestMatcher.matches(request);
}
public String toString() {
return "[ " + this.requestMatcher + ", " + this.filters + "]";
}
}
总结:通过此章节,我们对SpringSecurity工作原理有了一定的认识。但理论千万条,功能第一条,探寻底层,是为了更好的使用框架。
那么,言归正传!到底如何使用自己的页面来实现SpringSecurity的认证操作呢?要完成此功能,首先要有一套自己的页面!
4、SpringSecurity使用自定义认证页面
4.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: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">
<!--释放静态资源
注意:SpringMvc中也有释放静态资源,但是和这里的不同
SpringMVC释放静态资源是为了不让前端控制器去拦截静态资源
而这里的释放静态资源是为了不让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"/>
<!--配置springSecurity-->
<!--
auto-config="true" 表示自动加载springsecurity的配置文件
use-expressions="true" 表示使用spring的el表达式来配置springsecurity
-->
<security:http auto-config="true" use-expressions="true">
<!--让认证页面可以匿名访问
表示login.jsp页面不登陆就可以访问,但是依然会经过后续的过滤器链,因为接下来的操作都是Spring Security帮我们处理的,所以必须要经过
-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--拦截资源-->
<!--
pattern="/**" 表示拦截所有资源
access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
<!--配置认证信息
login-page:配置登录页面
login-processing-url:配置执行登录操作的请求路径,login是默认的,我们可以直接把自己的登录改成login就可以
default-target-url:登录成功默认跳转的地址
authentication-failure-url:登录(认证)失败跳转路径
username-parameter:form表单登录时,input的name属性的值<input name="username"/>,username是默认的,可以自定义
password-parameter:form表单登录时,input的name属性的值<input name="password"/>,password是默认的,可以自定义
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
username-parameter="username"
password-parameter="password" />
<!--配置退出登录信息
logout-url:配置登出操作的请求路径
logout-success-url:登出成功后请求的路径,这里设置成登出后跳转到登录页面
-->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
</security:http>
<!--设置Spring Security认证用户信息的来源-->
<!--
springsecurity默认的认证必须是加密的,加上{noop}表示不加密认证。
-->
<security:authentication-manager>
<!--现在在内存中配置两个用户-->
<security:authentication-provider>
<security:user-service>
<security:user name="user" password="{noop}user" authorities="ROLE_USER"/>
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
4.2 修改login.jsp
上面的配置了登录页面是我们自己的login.jsp,所以接下来访问的登录页面就不会是Spring Security自己的了,而是要访问我们自己的登录页面login.jsp。不过我们需要把登录的请求改成/login(这是Spring Security默认的,他会帮我们处理这个请求),并且提交方式必须是post。
<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>
4.3 为什么会是403
上述修改完成,启动项目,会到我们自己的登录页面:
使用user/user登录,结果却是403
这是为什么呢?
还记得我们前面查看他默认的登录页面的源码的时候,有这么一行代码:
<input name="_csrf" type="hidden" value="5f4a7ee0-1494-483b-af32-98670facbebf">
我们的login.jsp页面里面根本没加上这个的,所以就出现403。因为Spring Security会检查post请求是否携带了csrf的token,如果没有,就403。这是为了防止csrf攻击。
4.4 解决403
可以参考这篇博客,对于前后端不分离和前后端分离的,这里都讲到了解决方案:https://www.jianshu.com/p/969784677864
既然上面出现了403,那么应该如何解决呢?
有两种办法:一:关掉csrf检查,二:在login.jsp页面加上csrf
4.4.1 关掉csrf检查
修改spring-security.xml文件,在security:http 中加上:
<!--去掉csrf拦截的过滤器,直接去掉的这种方式不建议使用,因为可能存在CSRF攻击-->
<security:csrf disabled="true"/>
再次启动项目,登录就是正常的了,但是请注意,我们的系统是存在csrf攻击的。
4.4.2 jsp页面添加<security:csrfInput/>
上面的方式虽然可以达到我们的登录成功的目的,但是是不安全的,存在csrf攻击。那么如何去安全的解决这个问题?
如果项目是前后端不分离的,就跟我这个示例代码一样,那么只需要在jsp页面中添加下面的代码:
<%--引入security的标签,security这个的取值是任意的--%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<form action="${pageContext.request.contextPath}/login" method="post">
<%--csrf--%>
<security:csrfInput/>
如果是前后端分离的项目,请参考:https://www.jianshu.com/p/969784677864
4.5 SpringSecurity的csrf防护机制
CSRF(Cross-site request forgery)跨站请求伪造,是一种难以防范的网络攻击方式。
4.5.1 SpringSecurity中CsrfFilter过滤器说明
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.web.csrf;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
public final class CsrfFilter extends OncePerRequestFilter {
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
private final Log logger = LogFactory.getLog(this.getClass());
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher;
private AccessDeniedHandler accessDeniedHandler;
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
this.accessDeniedHandler = new AccessDeniedHandlerImpl();
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
//通过这里可以看出SpringSecurity的csrf机制把请求方式分成两类来处理
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//第一类:"GET", "HEAD", "TRACE", "OPTIONS"四类请求可以直接通过
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
//第二类:除去上面四类,包括POST都要被验证携带token才能通过
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
} else {
filterChain.doFilter(request, response);
}
}
}
public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
}
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
}
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods;
private DefaultRequiresCsrfMatcher() {
this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
}
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
}
注:HttpSessionCsrfTokenRepository对象负责生成token并放入session域中。
通过源码分析,我们明白了,自己的认证页面,请求方式为POST(“GET”, “HEAD”, “TRACE”, “OPTIONS” 除了这四种请求之外的,都需要加上token),但却没有携带token,所以才出现了403权限不足的异常。那么如何处理这个问题呢?
方式一:直接禁用csrf,不推荐。
方式二:在认证页面携带token请求。
4.6 退出登录
修改header.jsp的注销按钮:
5、SpringSecurity使用数据库数据完成认证
上面的示例中,我们一直是使用在内存中配置的用户进行认证和授权,实际开发中,肯定是应该查询数据库的用户、角色和权限表来实现的。所以我们需要对使用数据库完成认证的流程先进行分析:
5.1 认证流程分析
5.1.1 UsernamePasswordAuthenticationFilter
先看主要负责认证的过滤器UsernamePasswordAuthenticationFilter
干了啥,代码有删减,注意注释。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";//从这里看出默认的用户名是username
private String passwordParameter = "password";//从这里看出默认的密码是password
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
//可以知道默认的登录请求是 /login
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//可以看出,登录操作 必须为POST请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
//去掉用户名的首尾空格
username = username.trim();
//将填写的用户名和密码封装到了UsernamePasswordAuthenticationToken中
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
//调用AuthenticationManager对象的authenticate方法 实现认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
5.1.2 AuthenticationManager的实现类ProviderManager
由上面源码得知,真正认证操作在AuthenticationManager里面!
AuthenticationManager 是一个接口,所以我们应该看AuthenticationManager的实现类ProviderManager:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher;
private List<AuthenticationProvider> providers;
protected MessageSourceAccessor messages;
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication;
//注意AuthenticationProvider这个对象,SpringSecurity针对每一种认证,什么qq登录啊、用户名密码登陆啊、微信登录啊 等等登录方式 都封装了一个AuthenticationProvider对象。
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, (AuthenticationManager)null);
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
//循环所有 AuthenticationProvider,匹配当前认证类型
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
//找到了对应认证类型就继续调用AuthenticationProvider对象完成认证业务。
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (InternalAuthenticationServiceException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
}
5.1.3 AbstractUserDetailsAuthenticationProvider
从上面的这句代码:
//provider是某种认证方式对象, authentication 是我们前面传过来的UsernamePasswordAuthenticationToken对象
result = provider.authenticate(authentication);
点进去,看到的是一个接口:
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
这个接口有很多的实现类,针对不同的登录方式对应不同的实现类,我们本次使用的用户名密码登录,使用的实现类是:AbstractUserDetailsAuthenticationProvider(等会可以通过debug的方式验证)
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
protected final Log logger = LogFactory.getLog(this.getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
public AbstractUserDetailsAuthenticationProvider() {
}
protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
public final void afterPropertiesSet() throws Exception {
Assert.notNull(this.userCache, "A user cache must be set");
Assert.notNull(this.messages, "A message source must be set");
this.doAfterPropertiesSet();
}
//认证的方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//这个方法是执行真正的认证操作的,在本类中,是个抽象方法,所以我们需要看它的实现类
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
//这是抽象方法,由子类实现,我们查看子类的代码
protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
}
5.1.4 DaoAuthenticationProvider
DaoAuthenticationProvider是AbstractUserDetailsAuthenticationProvider的唯一子类,源码如下:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
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);
//若返回null,就抛出异常,认证失败。
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
//若有得到了UserDetails对象,返回即可。
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
}
5.1.5 AbstractUserDetailsAuthenticationProvider中authenticate返回值
按理说到此已经知道自定义认证方法的怎么写了,但咱们把返回的流程也大概走一遍,上面不是说到返回了一个
UserDetails对象对象吗?跟着它,就又回到了AbstractUserDetailsAuthenticationProvider对象中authenticate方法的最后一行了。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//。。。。这里省略了很多行
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
//。。。。这里省略了很多行
//最后一行返回值,调用了createSuccessAuthentication方法,此方法就在下面!
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
//咿!?怎么又封装了一次UsernamePasswordAuthenticationToken,开局不是已经封装过了吗?
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
//那就从构造方法点进去看看,这才干啥了。//认证成功后,调用的是这个带有三个参数的
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
5.1.6 UsernamePasswordAuthenticationToken
来到UsernamePasswordAuthenticationToken对象发现里面有两个构造方法
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 510L;
private final Object principal;
private Object credentials;
//认证成功前,调用的是这个带有两个参数的。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
//认证成功后,调用的是这个带有三个参数的
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
//看看父类干了什么!
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
}
5.1.7 AbstractAuthenticationToken
再点进去super(authorities)看看
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
private final Collection<GrantedAuthority> authorities;
private Object details;
private boolean authenticated = false;
public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
//这是两个参数那个分支!
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
} else {
//三个参数的,看这里!
Iterator var2 = authorities.iterator();
//原来是多个了添加权限信息的步骤
GrantedAuthority a;
do {
if (!var2.hasNext()) {
ArrayList<GrantedAuthority> temp = new ArrayList(authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);
return;
}
a = (GrantedAuthority)var2.next();
} while(a != null);
//若没有权限信息,是会抛出异常的!
throw new IllegalArgumentException("Authorities collection cannot contain any null elements");
}
}
}
由此,咱们需要牢记自定义认证业务逻辑返回的UserDetails对象中一定要放置权限信息啊!
现在可以结束源码分析了吧?先不要着急!
咱们回到最初的地方UsernamePasswordAuthenticationFilter,你看好看了,这可是个过滤器,咱们分析这么
久,都没提到doFilter方法,你不觉得心里不踏实?可是这里面也没有doFilter呀?那就从父类找!
5.1.8 AbstractAuthenticationProcessingFilter
点开AbstractAuthenticationProcessingFilter,删掉不必要的代码!
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
//...
public void afterPropertiesSet() {
Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
}
//doFilter在此
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
//失败走unsuccessfulAuthentication
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
//失败走unsuccessfulAuthentication
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//成功走successfulAuthentication
this.successfulAuthentication(request, response, chain, authResult);
}
}
//成功走successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
//认证成功,将认证信息存储到SecurityContext中!
SecurityContextHolder.getContext().setAuthentication(authResult);
//登录成功调用rememberMeServices
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
//失败走unsuccessfulAuthentication
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication request failed: " + failed.toString(), failed);
this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
}
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
}
可见AbstractAuthenticationProcessingFilter这个过滤器对于认证成功与否,做了两个分支,成功执行
successfulAuthentication,失败执行unsuccessfulAuthentication。
在successfulAuthentication内部,将认证信息存储到了SecurityContext中。并调用了loginSuccess方法,这就是常见的“记住我”功能!此功能具体应用,咱们后续再研究!
5.2 初步实现认证功能
5.2.1 让我们自己的UserService接口继承UserDetailsService
毕竟SpringSecurity是只认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);
}
5.2.2 在UserServiceImpl编写loadUserByUsername业务
/**
* Spring Security进行用户认证的方法
* @param username 页面传过来的用户名
* @return UserDetails UserDetails是Spring Security自己的用户对象,如果返回null就表示认证失败
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
//根据用户名从数据库中查询到用户信息
SysUser sysUser = userDao.findByName(username);
if(sysUser == null) {
//若用户名不对,直接返回null,表示认证失败。
return null;
}
//把sysUser封装成Spring Security可以认识的UserDetails对象
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
//得到当前用户的所有角色的集合
List<SysRole> roles = sysUser.getRoles();
//遍历角色(权限),存入到authorities中
for (SysRole role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
}
/**
* 最终需要返回一个SpringSecurity的UserDetails对象,{noop}表示不加密认证。
* 浏览器输入的密码已经被Spring Security保存了,这里封装之后,会进行比对,如果是相同的就是认证通过,否则认证失败
*/
UserDetails userDetails = new User(username,"{noop}"+sysUser.getPassword(),authorities);
return userDetails;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
5.2.3 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: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">
<!--释放静态资源
注意:SpringMvc中也有释放静态资源,但是和这里的不同
SpringMVC释放静态资源是为了不让前端控制器去拦截静态资源
而这里的释放静态资源是为了不让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"/>
<!--配置springSecurity-->
<!--
auto-config="true" 表示自动加载springsecurity的配置文件
use-expressions="true" 表示使用spring的el表达式来配置springsecurity
-->
<security:http auto-config="true" use-expressions="true">
<!--让认证页面可以匿名访问
表示login.jsp页面不登陆就可以访问,但是依然会经过后续的过滤器链,因为接下来的操作都是Spring Security帮我们处理的,所以必须要经过
-->
<security:intercept-url pattern="/login.jsp" access="permitAll()"/>
<!--拦截资源-->
<!--
pattern="/**" 表示拦截所有资源
access="hasAnyRole('ROLE_USER')" 表示只有ROLE_USER角色才能访问资源
-->
<security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER')"/>
<!--配置认证信息
login-page:配置登录页面
login-processing-url:配置执行登录操作的请求路径,login是默认的,我们可以直接把自己的登录改成login就可以
default-target-url:登录成功默认跳转的地址
authentication-failure-url:登录(认证)失败跳转路径
username-parameter:form表单登录时,input的name属性的值<input name="username"/>,username是默认的,可以自定义
password-parameter:form表单登录时,input的name属性的值<input name="password"/>,password是默认的,可以自定义
-->
<security:form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
username-parameter="username"
password-parameter="password" />
<!--配置退出登录信息
logout-url:配置登出操作的请求路径
logout-success-url:登出成功后请求的路径,这里设置成登出后跳转到登录页面
-->
<security:logout logout-url="/logout"
logout-success-url="/login.jsp"/>
<!--去掉csrf拦截的过滤器,直接去掉的这种方式不建议使用,因为可能存在CSRF攻击-->
<!--<security:csrf disabled="true"/>-->
</security:http>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
</security:authentication-provider>
</security:authentication-manager>
</beans>
主要修改的是<security:authentication-manager>,其他不变。
5.3 加密认证
目前我们的数据库中,保存的密码是明文的,这样显然是不安全的。所以应该把用户的密码加密之后才能保存到数据库,同时在认证的时候,也应该是验证这个加密之后的密码。
为此Spring Security给我们提供了一种加密方式:BCryptPasswordEncoder,它的特点是:同样的密码,比如123,每次加密得到的密文是不同的。对于密码的验证,它提供了一个matches方法,该方法有两个参数,参数一是密码的明文,参数二是加密之后的密码。方法返回一个boolean值。
它和MD5加密不同,MD5加密123得到的密文是相同的。因此现在的用法是加盐加密。而BCryptPasswordEncoder也是采用了加盐加密方式,但是它是动态加盐,每次加的盐都不同,所以每次的密文不同。
package org.springframework.security.crypto.bcrypt;
import java.security.SecureRandom;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
public class BCryptPasswordEncoder implements PasswordEncoder {
private Pattern BCRYPT_PATTERN;
private final Log logger;
private final int strength;
private final SecureRandom random;
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
this(strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
this.logger = LogFactory.getLog(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.strength = strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}
//密码加密的方法,参数是明文密码,返回值是加密之后的密码
public String encode(CharSequence rawPassword) {
String salt;
if (this.strength > 0) {
if (this.random != null) {
salt = BCrypt.gensalt(this.strength, this.random);
} else {
salt = BCrypt.gensalt(this.strength);
}
} else {
salt = BCrypt.gensalt();
}
return BCrypt.hashpw(rawPassword.toString(), salt);
}
//验证密码的方法,参数一是明文密码,参数二是加密之后的密码,返回值是是否验证成功
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
}
5.3.1 在IOC容器中提供加密对象
修改spring-security.xml文件
<!--把加密对象放入的IOC容器中-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<!--如果BCryptPasswordEncoder的id属性是passwordEncoder,那么这句可以不加,不过BCryptPasswordEncoder的配置要在这个的前面,但是建议都加上-->
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
5.3.2 修改上面的loadUserByUsername方法
修改认证方法loadUserByUsername ,去掉{noop}
UserDetails userDetails = new User(username,sysUser.getPassword(),authorities);
return userDetails;
5.3.3 修改添加用户的方法
添加用户的时候,应该把密码加密:
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@RequestMapping("/save")
public String save(SysUser user){
//加密密码
String encodePassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodePassword);
userService.save(user);
return "redirect:findAll";
}
5.3.4 测试
改完之后,重启项目,输入xiaoming/123456 你会发现登录失败了。这是为啥呢?
因为现在是开启的加密认证,Spring Security会认为你的数据库密码是加密的,但是现在我们存的是明文,所以导致了认证失败。
为了测试,我们需要手动把123456加密,然后手动修改数据库:
public class PasswordTest {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);
}
}
然后手动修改数据库用户的密码
再次登录的话,应该就是正常的了。
sword");
return false;
}
}
}
### 5.3.1 在IOC容器中提供加密对象
修改spring-security.xml文件
```xml
<!--把加密对象放入的IOC容器中-->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--设置Spring Security认证用户信息的来源-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userServiceImpl">
<!--如果BCryptPasswordEncoder的id属性是passwordEncoder,那么这句可以不加,不过BCryptPasswordEncoder的配置要在这个的前面,但是建议都加上-->
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
5.3.2 修改上面的loadUserByUsername方法
修改认证方法loadUserByUsername ,去掉{noop}
UserDetails userDetails = new User(username,sysUser.getPassword(),authorities);
return userDetails;
5.3.3 修改添加用户的方法
添加用户的时候,应该把密码加密:
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@RequestMapping("/save")
public String save(SysUser user){
//加密密码
String encodePassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodePassword);
userService.save(user);
return "redirect:findAll";
}
5.3.4 测试
改完之后,重启项目,输入xiaoming/123456 你会发现登录失败了。这是为啥呢?
因为现在是开启的加密认证,Spring Security会认为你的数据库密码是加密的,但是现在我们存的是明文,所以导致了认证失败。
为了测试,我们需要手动把123456加密,然后手动修改数据库:
public class PasswordTest {
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);
}
}
然后手动修改数据库用户的密码
再次登录的话,应该就是正常的了。