目标:初步掌握Spring Security的认证功能实现。
在权限管理的概念中,有两个非常重要的名词:
认证:通过用户名和密码(当然也可以是其它方式,比如邮箱、身份证等)成功登录系统后,让系统得到当前用户的角色身份。
授权:系统根据当前用户的角色,给其授予对应可以操作的权限资源。
一般而言,用户可以分配多个角色,角色可以分配多个权限。所以,在权限设计表的时候,一般设计5张表,分别为用户表、角色表、权限表、用户角色表、角色权限表。业内有时也会将这5张表称为经典的RBAC权限设计模型。
<!-- spring-security-config是用来解析XML配置文件 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.1.5.RELEASE</version> </dependency> <!-- spring-security-core是Spring Security的核心jar包,任何Spring Security都需要用此jar包 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>5.1.5.RELEASE</version> </dependency> <!-- spring-security-taglibs是Spring Security提供的动态标签库,JSP页面中可以使用。 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>5.1.5.RELEASE</version> </dependency> <!-- spring-security-web是web工程的必备jar包,包含过滤器和相关的web安全基础结构代码 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.1.5.RELEASE</version> </dependency>
<!-- 配置Spring Security的核心过滤器链 --> <!-- filter-name必须是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>
<?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"> <!-- 配置Spring Security auto-config="true"表示自定加载spring-security.xml配置文件 use-expressions="true"表示使用spring的el表达式来配置spring security --> <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认证用户信息的来源--> <!-- Spring Security的认证必须是加密的,{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>
<!--引入SpringSecurity主配置文件--> <import resource="classpath:spring-security.xml"/>
INFO web.DefaultSecurityFilterChain - Creating filter chain: any request, [org.springframework.security.web.context.SecurityContextPersistenceFilter@17455fed, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@97f4fa3, org.springframework.security.web.header.HeaderWriterFilter@1384edda, org.springframework.security.web.csrf.CsrfFilter@544ac02d, org.springframework.security.web.authentication.logout.LogoutFilter@694d477e, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1abc0fa3, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2e62db13, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@2c2b3d5a, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2fdef916, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@55e53a59, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@6d67c9b5, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@18d3e8ae, org.springframework.security.web.session.SessionManagementFilter@409db4eb, org.springframework.security.web.access.ExceptionTranslationFilter@3cd67953, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@6faf351d]
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用个,为后续Filter建立所需要的上下文。SecurityContext中存储了当前用户的认证和权限信息。
此过滤器用语集成SecurityContext到Spring异步执行机制中的WebAsyncManager。
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制。
csrf又称为跨域请求伪造,SpringSecurity会对所有POST、PUT、DELETE请求验证是否包含系统生成的csrf的token信息,如果不包含,就报错。起到防止csrf攻击的效果。
匹配URL为/logout的请求,实现用户退出,清除认证信息。
认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
如果没有在配置文件中执行认证页面,则由该过滤器生成一个默认认证页面。
此过滤器产生的一个默认的退出登录的页面。
此过滤器会自动解析HTTP请求中头部带有Authentication,且以Basic开头的头信息。
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest。
针对ServletRequest进行了一次包装,使得request具有更加丰富的API。
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中,SpringSecurity为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名身份。
SecurityContextRepository限制同一用户开启多个会话的数量。
异常转换过滤器位于整个SpringSecurityFilterChain的后方,用来转换整个链路中出现的异常。
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。
public class DelegatingFilterProxy extends GenericFilterBean { @Nullable private String contextAttribute; @Nullable private WebApplicationContext webApplicationContext; @Nullable private String targetBeanName; private boolean targetFilterLifecycle = false; @Nullable private volatile Filter delegate; //注意:这个过滤器才是真正加载的过滤器 private final Object delegateMonitor = new Object(); //注意:doFilter是过滤器的入口,直接从这边看。 @Override 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 = findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: " + "no ContextLoaderListener or DispatcherServlet registered?"); } //第一步:doFilter中最重要的一步,初始化上面私有过滤器属性delegate delegateToUse = initDelegate(wac); } this.delegate = delegateToUse; } } //第三步:执行FilterChainProxy过滤器 invokeDelegate(delegateToUse, request, response, filterChain); } //第二步:直接看最终加载的过滤器是谁 protected Filter initDelegate(WebApplicationContext wac) throws ServletException { //debug得知targetBeanName为springSecurityFilterChain String targetBeanName = getTargetBeanName(); Assert.state(targetBeanName != null, "No target bean name set"); //debug得知Filter对象为FilterChainProxy Filter delegate = wac.getBean(targetBeanName, Filter.class); if (isTargetFilterLifecycle()) { delegate.init(getFilterConfig()); } return delegate; } protected void invokeDelegate( Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { delegate.doFilter(request, response, filterChain); } }
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对象,可能SecurityFilterChain才是真正的过滤器对象。 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); 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); } } 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(); } }
//接口 public interface SecurityFilterChain { boolean matches(HttpServletRequest var1); List<Filter> getFilters(); } //实现类 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 + "]"; } }
<?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过滤器的静态资源--> <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 pattern="/favicon.ico" security="none"/> <!-- 配置Spring Security auto-config="true"表示自定加载spring-security.xml配置文件 use-expressions="true"表示使用spring的el表达式来配置spring security --> <security:http auto-config="true" use-expressions="true"> <!--指定login.jsp页面可以被匿名访问--> <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 处理登录的处理器的地址 default-target-url 登录成功跳转的地址 authentication-failure-url 登录失败跳转的地址 默认的用户名是username,密码是password,当然也可以使用username-parameter和password-parameter修改。 --> <security:form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/index.jsp" authentication-failure-url="/failer.jsp"/> <!-- 指定退出登录后跳转的页面 logout-url 处理退出登录的处理器地址 logout-success-url 退出登录成功跳转的地址 --> <security:logout logout-url="/logout" logout-success-url="/login.jsp"/> </security:http> <!--设置Spring Security认证用户信息的来源--> <!-- Spring Security的认证必须是加密的,{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>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>数据 - AdminLTE2定制版 | Log in</title> <meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css"> </head> <body class="hold-transition login-page"> <div class="login-box"> <div class="login-logo"> <a href="#"><b></b>后台管理系统</a> </div> <!-- /.login-logo --> <div class="login-box-body"> <p class="login-box-msg">登录系统</p> <!-- action的地址必须是/login method 必须是post --> <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> <!-- /.login-box-body --> </div> <!-- /.login-box --> <!-- jQuery 2.2.3 --> <!-- Bootstrap 3.3.6 --> <!-- iCheck --> <script src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script> <script src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script> <script src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script> <script> $(function() { $('input').iCheck({ checkboxClass : 'icheckbox_square-blue', radioClass : 'iradio_square-blue', increaseArea : '20%' // optional }); }); </script> </body> </html>
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; } //从这里可以看出Spring Security的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、DELETE、PUT等都需要携带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()); } } }
通过源码,我们知道,我们自己的登录页面的请求方式是POST,但是却没有携带token,所以才会出现403权限不足的异常,那么如何处理?
<?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过滤器的静态资源--> <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 pattern="/favicon.ico" security="none"/> <!-- 配置Spring Security auto-config="true"表示自定加载spring-security.xml配置文件 use-expressions="true"表示使用spring的el表达式来配置spring security --> <security:http auto-config="true" use-expressions="true"> <!--指定login.jsp页面可以被匿名访问--> <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 处理登录的处理器的地址 default-target-url 登录成功跳转的地址 authentication-failure-url 登录失败跳转的地址 默认的用户名是username,密码是password,当然也可以使用username-parameter和password-parameter修改。 --> <security:form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/index.jsp" authentication-failure-url="/failer.jsp"/> <!-- 指定退出登录后跳转的页面 logout-url 处理退出登录的处理器的地址 logout-success-url 退出登录成功跳转的地址 --> <security:logout logout-url="/logout" logout-success-url="/login.jsp"/> <!-- 禁用csrf防护机制 --> <security:csrf disabled="true"/> </security:http> <!--设置Spring Security认证用户信息的来源--> <!-- Spring Security的认证必须是加密的,{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>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%-- 添加标签库 --%> <%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>数据 - AdminLTE2定制版 | Log in</title> <meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/ionicons/css/ionicons.min.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/adminLTE/css/AdminLTE.css"> <link rel="stylesheet" href="${pageContext.request.contextPath}/plugins/iCheck/square/blue.css"> </head> <body class="hold-transition login-page"> <div class="login-box"> <div class="login-logo"> <a href="#"><b></b>后台管理系统</a> </div> <!-- /.login-logo --> <div class="login-box-body"> <p class="login-box-msg">登录系统</p> <form action="${pageContext.request.contextPath}/login" method="post"> <%-- 在认证页面携带token --%> <security:csrfInput/> <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> <!-- /.login-box-body --> </div> <!-- /.login-box --> <!-- jQuery 2.2.3 --> <!-- Bootstrap 3.3.6 --> <!-- iCheck --> <script src="${pageContext.request.contextPath}/plugins/jQuery/jquery-2.2.3.min.js"></script> <script src="${pageContext.request.contextPath}/plugins/bootstrap/js/bootstrap.min.js"></script> <script src="${pageContext.request.contextPath}/plugins/iCheck/icheck.min.js"></script> <script> $(function() { $('input').iCheck({ checkboxClass : 'icheckbox_square-blue', radioClass : 'iradio_square-blue', increaseArea : '20%' // optional }); }); </script> </body> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@taglib prefix="security" uri="http://www.springframework.org/security/tags" %> <!-- 页面头部 --> <header class="main-header"> <!-- Logo --> <a href="${pageContext.request.contextPath}/pages/main.jsp" class="logo"> <!-- mini logo for sidebar mini 50x50 pixels --> <span class="logo-mini"><b>数据</b></span> <!-- logo for regular state and mobile devices --> <span class="logo-lg"><b>数据</b>后台管理</span> </a> <!-- Header Navbar: style can be found in header.less --> <nav class="navbar navbar-static-top"> <!-- Sidebar toggle button--> <a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button"> <span class="sr-only">Toggle navigation</span> </a> <div class="navbar-custom-menu"> <ul class="nav navbar-nav"> <li class="dropdown user user-menu"><a href="#" class="dropdown-toggle" data-toggle="dropdown"> <img src="${pageContext.request.contextPath}/img/user2-160x160.jpg" class="user-image" alt="User Image"> <span class="hidden-xs"> <%--<security:authentication property="principal.username" />--%> <%--<security:authentication property="name" />--%> </span> </a> <ul class="dropdown-menu"> <!-- User image --> <li class="user-header"><img src="${pageContext.request.contextPath}/img/user2-160x160.jpg" class="img-circle" alt="User Image"></li> <!-- Menu Footer--> <li class="user-footer"> <div class="pull-left"> <a href="#" class="btn btn-default btn-flat">修改密码</a> </div> <div class="pull-right"> <%-- 将原来的注销注释,使用form表单的形式提交,在表单携带token请求 --%> <%-- <a href="${pageContext.request.contextPath}/login.jsp"--%> <%-- class="btn btn-default btn-flat">注销</a>--%> <form action="${pageContext.request.contextPath}/logout" method="post"> <security:csrfInput/> <input type="submit" class="btn btn-default btn-flat">注销</input> </form> </div> </li> </ul> </li> </ul> </div> </nav> </header> <!-- 页面头部 /-->
UsernamePasswordAuthenticationFilter是用来负责认证的过滤器。
package org.springframework.security.web.authentication; 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"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { 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对应进行认证 return this.getAuthenticationManager().authenticate(authRequest); } } protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return this.usernameParameter; } public final String getPasswordParameter() { return this.passwordParameter; } }
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,Spring Security针对每一种认证,什么QQ登录,微信登录都封装到一个AuthenticationProvider对象中 public ProviderManager(List<AuthenticationProvider> providers) { this(providers, (AuthenticationManager)null); } public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) { this.eventPublisher = new ProviderManager.NullEventPublisher(); this.providers = Collections.emptyList(); this.messages = SpringSecurityMessageSource.getAccessor(); this.eraseCredentialsAfterAuthentication = true; Assert.notNull(providers, "providers list cannot be null"); this.providers = providers; this.parent = parent; this.checkState(); } public void afterPropertiesSet() throws Exception { this.checkState(); } private void checkState() { if (this.parent == null && this.providers.isEmpty()) { throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required"); } } //认证的方法 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; } } private void prepareException(AuthenticationException ex, Authentication auth) { this.eventPublisher.publishAuthenticationFailure(ex, auth); } private void copyDetails(Authentication source, Authentication dest) { if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) { AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest; token.setDetails(source.getDetails()); } } public List<AuthenticationProvider> getProviders() { return this.providers; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) { Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null"); this.eventPublisher = eventPublisher; } public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) { this.eraseCredentialsAfterAuthentication = eraseSecretData; } public boolean isEraseCredentialsAfterAuthentication() { return this.eraseCredentialsAfterAuthentication; } private static final class NullEventPublisher implements AuthenticationEventPublisher { private NullEventPublisher() { } public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) { } public void publishAuthenticationSuccess(Authentication authentication) { } } }
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 { //获取UserDetails对象,即SpringSecurity自己的用户对象 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; }
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 void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { this.logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } } ////获取UserDetails对象,即SpringSecurity自己的用户对象 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { //UserDetails对象,即SpringSecurity自己的用户对象 //loadUserByUsername是真正的认证逻辑,即我们可以直接编写一个UserDetailsService()的实现呢类,告诉SpringSecurity就可以了。 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); } } }
package com.weiwei.xu.service; import com.weiwei.xu.domain.SysUser; import org.springframework.security.core.userdetails.UserDetailsService; import java.util.List; import java.util.Map; 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); }
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userDao.findByName(username); if (null == sysUser) { //如果用户名不对,直接返回null,表示认证失败 return null; } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); List<SysRole> roles = sysUser.getRoles(); if (null != roles && roles.size() != 0) { roles.forEach(role -> { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName()); authorities.add(simpleGrantedAuthority); }); } //返回UserDetails对象,"{noop}"+密码表示不加密认证 UserDetails userDetails = new User(sysUser.getUsername(), "{noop}" + sysUser.getPassword(), authorities); return userDetails; } }
<?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过滤器的静态资源--> <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 pattern="/favicon.ico" security="none"/> <!-- 配置Spring Security auto-config="true"表示自定加载spring-security.xml配置文件 use-expressions="true"表示使用spring的el表达式来配置spring security --> <security:http auto-config="true" use-expressions="true"> <!--指定login.jsp页面可以被匿名访问--> <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 处理登录的处理器的地址 default-target-url 登录成功跳转的地址 authentication-failure-url 登录失败跳转的地址 默认的用户名是username,密码是password,当然也可以使用username-parameter和password-parameter修改。 --> <security:form-login login-page="/login.jsp" login-processing-url="/login" default-target-url="/index.jsp" authentication-failure-url="/failer.jsp"/> <!-- 指定退出登录后跳转的页面 logout-url 处理退出登录的处理器的地址 logout-success-url 退出登录成功跳转的地址 --> <security:logout logout-url="/logout" logout-success-url="/login.jsp"/> <!-- 禁用csrf防护机制 --> <!-- <security:csrf disabled="true"/>--> </security:http> <!--设置Spring Security认证用户信息的来源--> <!-- Spring Security的认证必须是加密的,{noop}表示不加密认证 --> <security:authentication-manager> <security:authentication-provider user-service-ref="userServiceImpl"> </security:authentication-provider> </security:authentication-manager> </beans>
<!-- 加密对象 --> <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean> <!--设置Spring Security认证用户信息的来源--> <!-- Spring Security的认证必须是加密的,{noop}表示不加密认证 --> <security:authentication-manager> <security:authentication-provider user-service-ref="userServiceImpl"> <!-- 指定认证使用的加密对象 --> <security:password-encoder ref="passwordEncoder"/> </security:authentication-provider> </security:authentication-manager>
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userDao.findByName(username); if (null == sysUser) { //如果用户名不对,直接返回null,表示认证失败 return null; } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); List<SysRole> roles = sysUser.getRoles(); if (null != roles && roles.size() != 0) { roles.forEach(role -> { SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getRoleName()); authorities.add(simpleGrantedAuthority); }); } //返回UserDetails对象,"{noop}"+密码表示不加密认证 UserDetails userDetails = new User(sysUser.getUsername(), sysUser.getPassword(), authorities); return userDetails; }
@Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Override public void save(SysUser user) { user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); userDao.save(user); }
$2a$10$ynlaufZM048G5jsp98seeuvkAXNCVD5RFEudlrW.xiNihU.2Tjm9W