本文建立在 SSH與Spring Security整合 一文的基礎上,從這篇文章的example上做修改,或者從 配置了AOP 的example上做修改皆可。這里主要補充我在實際使用Spring Security中常用的一些前文最基本example中沒能提供的功能,主要包括自定義403錯誤頁面、自定義認證管理器的內容提供者、自定義登錄成功的回調接口,自定義json訪問時未登錄和403錯誤的返回內容和用代碼模擬Spring Security的驗證。
在搭建Spring Security的時候,http標簽內配置了這樣的子標簽:
<form-login login-page="/" default-target-url="/" authentication-failure-url="/?login=error" />
這個屬性是說,如果待訪問的資源需要一定的權限,但是當前用戶沒有登錄,那么應該跳轉到login-page上去登錄,如果登錄成功了,就跳轉到default-target-url上去,如果登錄失敗了,就跳轉到anthentication-failure-url上去,但是缺一個配置,那就是如果我登錄了,並且是USER權限,現在訪問了一個需要ADMIN權限的資源,那么怎么辦?實際中會返回一個默認的界面:
那么這個界面太丑了,怎么自定義,這個非常簡單,只需要在http標簽中加入下面的一個:
<access-denied-handler error-page="/denied"/>
也就是說,如果訪問權限不夠,就會訪問/denied這個資源,因為Springmvc會攔截所有的請求,這個也不例外,在HomeController中加入:
@RequestMapping("/denied") public String denied(){ return "denied"; }
在webapps/pages目錄下創建denied.jsp:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <c:set var="base" value="${pageContext.request.contextPath }/" scope="session"/> <html> <body> <h2>您的訪問權限不夠!!</h2> <h3>3秒鍾之后跳轉到首頁。。。或點擊<a href="${base }">首頁</a></h3> </body> <script type="text/javascript"> setTimeout(function(){ location.href = "${base }"; }, 3000); </script> </html>
再次訪問受限的資源就會跳轉到這個界面上。
先回顧一下前文中怎么做用戶名密碼驗證的:
<authentication-manager> <authentication-provider> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username, password, 1 from user where username = ?" authorities-by-username-query="select u.username, r.role from user u left join role r on u.role_id=r.id where username = ?" /> </authentication-provider> </authentication-manager>
指定數據源,根據用戶提交上來的用戶名發兩條sql語句,獲取到password和role,然后拿password和用戶提交的密碼(根據配置可能會做加鹽的處理)匹配,如果登錄成功,該用戶的信息就以role所代表的權限保存了起來,但是有時候,對用戶名密碼的獲取,不能夠通過簡單的兩條sql語句來獲取,那又應該怎么辦呢?這就需要我們來自定義了,基本思路是我們寫一個bean,Spring把用戶名給這個bean,這個bean自己去找密碼權限應該是什么,最后封裝成一個User對象返回給Spring,也就是說,我們需要寫自己的jdbc-user-service。下面就來實現它,創建一個package叫做security,再寫一個類EssentialUser,並實現Spring的UserDetails接口,這個類就是Spring最終需要的User對象:
package org.zhangfc.demo4ssh.security; import ......; public class EssentialUser implements UserDetails { private static final long serialVersionUID = -3369448632273314162L; private int id; private String role; private String username; private String password; public EssentialUser(User user) { this.id = user.getId(); this.role = user.getRole().getRole(); this.username = user.getUsername(); this.password = user.getPassword(); } // setter and getter of id, role // setter of username, password @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> auths = new ArrayList<>(); auths.add(new SimpleGrantedAuthority(this.role)); return auths; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
下面需要寫一個Service來把這個對象給Spring,還是在security包下面創建MyUserDetailsService,實現Spring的UserDetailsService接口,我這兒簡單起見了,我就只new了一個User對象,實際上應該是查詢好了必要信息的對象:
public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User u = new User(); // 根據username來得到User對象 EssentialUser eu = new EssentialUser(u); return eu; } }
剩下的就很簡單了,注冊一下這個bean,並把它作為認證信息的提供者:
<beans:bean id="userDetails" class="cn.edu.tju.ina.estuary.security.MyUserDetailsService" /> <authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="userDetails" /> </authentication-manager>
有時候,Spring存的那個UserDetails用戶信息不全,而且因為是Spring的接口,有時候用起來也不方便,我們希望在登錄成功之后再在session中存一份當前用戶對象,登錄成功之后Spring會跳轉到配置的URL上,但是很多時候,登錄成功就是跳回首頁,訪問首頁沒必要再分是不是剛登錄,所以要是Spring Security有登錄之后的回調接口,存session的工作就可以在那里做了,這個想法當然是可行的。在security這個package下創建類AfterAuthSuccess,繼承SimpleUrlAuthenticationSuccessHandler:
public class AfterAuthSuccess extends SimpleUrlAuthenticationSuccessHandler { @Autowired private UserService userService; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { RequestCache requestCache = new HttpSessionRequestCache(); SavedRequest savedRequest = requestCache.getRequest(request, response); HttpSession session = request.getSession(); SecurityContext sc = SecurityContextHolder.getContext(); String userName = sc.getAuthentication().getName(); User u = userService.findByUsername(userName); session.setAttribute("currentUser", u); if (savedRequest == null) { // if click login to open login page, savedRequest will be null. super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); String targetUrl = savedRequest.getRedirectUrl(); if(targetUrl != null && "".equals(targetUrl)){ super.onAuthenticationSuccess(request, response, authentication); return; } getRedirectStrategy().sendRedirect(request, response, targetUrl); } }
這段代碼可以直接用,看上去很復雜,是因為考慮了一些情況,比如訪問A頁面發現沒有登錄,這時候會跳轉到登錄頁面去登錄,登錄成功之后會直接跳到A上去。
然后在xml文件中配置一下這個bean:
<http auto-config="true"> <intercept-url pattern="/admin**" access="ROLE_ADMIN" /> <form-login login-page="/" authentication-success-handler-ref="authSuccess" default-target-url="/" authentication-failure-url="/?login=error" /> <access-denied-handler error-page="/denied"/> <logout logout-success-url="/" /> </http> <beans:bean id="authSuccess" class="org.zhangfc.demo4ssh.security.AfterAuthSuccess" />
web應用中有很多接口可能是為移動端設計的,移動端有自己的權限控制方案,或者web也可能頻繁請求json資源,那么對這些接口,未登錄的時候就不能再跳轉到login-page,權限不夠的時候也不能再返回個403頁面,這就需要自己來配置,原來有一個http標簽,用來處理所有的請求,現在在它前面加一個,只處理/json開頭的地址:
<http pattern="/json**" entry-point-ref="jsonEntryPoint"> <intercept-url pattern="/json**" access="ROLE_USER" /> <access-denied-handler error-page="/900" /> </http>
只有ROLE_USER權限是可以訪問這些資源的(ROLE_ADMIN也不行),如果是權限不夠呢,跳轉到/900,如果是未登錄,也就是Spring Security沒有存這個票據,那么Spring會扔出一個異常,扔到ExceptionTranslationFilter鏈里去,EntryPoint就是來處理這個問題的,來看這個引用的bean:
<beans:bean id="jsonEntryPoint" class="org.zhangfc.demo4ssh.security.JsonEntryPoint"> <beans:property name="url" value="/901"></beans:property> </beans:bean>
這兒指定當未登錄的時候請求/901。看看這個bean怎么來實現:
public class JsonEntryPoint implements AuthenticationEntryPoint { private String url = "/"; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { request.getRequestDispatcher(url).include(request, response); } public void setUrl(String url) { this.url = url; } }
非常簡單,這個AuthenticationException還可以拿來做一些更細致的判斷,不過我沒有去做太多嘗試。
最后只要在/900和/901的Controller里面返回對應的json串就可以了。
前面的介紹都是Spring自己去校驗用戶名密碼之后就登錄了,有時候我們需要模擬Spring登錄,比如注冊之后直接變成登錄狀態,當然也可以用代碼發一個登錄請求,不過有些麻煩,不如直接用代碼來登錄,其實也很簡單:
@Autowired @Qualifier("authenticationManager") protected AuthenticationManager authenticationManager; private void setAuthInSpringSecuity(String username, String password, HttpServletRequest request) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( username, password); try { token.setDetails(new WebAuthenticationDetails(request)); Authentication authenticatedUser = authenticationManager .authenticate(token); SecurityContextHolder.getContext().setAuthentication( authenticatedUser); request.getSession() .setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); } catch (AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } }
這兒就只把代碼貼在這里了,沒有什么需要解釋的,Spring驗證登錄成功之后會把當前用戶對象放到session里,最后幾行做的就是這個事情。