目標:初步掌握Spring Security的認證功能實現。
在權限管理的概念中,有兩個很是重要的名詞:認證:經過用戶名和密碼(固然也能夠是其它方式,好比郵箱、身份證等)成功登陸系統後,讓系統獲得當前用戶的角色身份。css
受權:系統根據當前用戶的角色,給其授予對應能夠操做的權限資源。html
通常而言,用戶能夠分配多個角色,角色能夠分配多個權限。因此,在權限設計表的時候,通常設計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