在之前的一個項目中有使用到,方便以後項目框架的搭建,這裏還是再總結一下。
Spring Security 的認證流程
-
用戶發出登錄請求。
-
首先經過 SecurityContextPersistenceFilter 過濾器,將 Session 中的認證信息保存到 SecurityContextHodlder 中。
-
然後在認證邏輯過濾器UsernamePasswordAuthenticationFilter,封裝令牌 Token,設置請求信息等。一般我們需要自定義認證邏輯,所以需要繼承並重寫。
-
然後通過 AuthenticationManager 認證管理器,遍歷所有的 AuthenticationProvider,找到支持該 Token 的認證提供者即 AbstractUserDetailsAuthenticationProvide。
-
AbstractUserDetailsAuthenticationProvider 會調用它的子類 DaoAuthenticationProvider 的 retrieveUser 方法來獲取用戶信息 UserDetails。
-
而 DaoAuthenticationProvider 會調用 UserDetailsService 接口的 loadUserByUsername 方法來獲取用戶信息 UserDetails,我們只要實現 UserDetailsService 接口,寫獲取用戶信息的邏輯就可以了。
-
如果整個過程都沒有異常,則認證通過,最終將認證結果 Authentication 保存到 SecurityContext 中,然後將 SecurityContext 保存到 SecurityContextHolder 中。
-
再次經過 SecurityContextPersistenceFilter 過濾器時,將 SecurityContextHolder 中的 SecurityContext 保存到 Session 中,清空 SecurityContextHolder 中的內容,這樣就記住了當前用戶的登錄狀態。
Spring 中整合 Spring security
引入 jar 包就不說了,還有角色 Role,權限 Permission這裏就不說了。
首先是我們的 User 實體類需要實現 UserDetails 接口,並重寫所有方法:
public class User implements UserDetails {
private Long id;
private String email;
private String password;
private String phone;
private String nickName;
private String state;
private String imgUrl;
private String enable;
@Transient
protected List<Role> roles;
//getter/setter方法...
//重寫所有接口方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {//返回該用戶擁有的權限
if(roles == null || roles.size()<=0){
return null;
}
List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
for(Role r:roles){
authorities.add(new SimpleGrantedAuthority(r.getRoleValue()));
}
return authorities;
}
@Override
public String getUsername() {//獲取用戶名
return email;
}
@Override
public boolean isAccountNonExpired() {// 帳戶是否過期
return true;
}
@Override
public boolean isAccountNonLocked() {// 帳戶是否被凍結
return true;
}
@Override
public boolean isCredentialsNonExpired() {// 帳戶密碼是否過期
return true;
}
@Override
public boolean isEnabled() {// 帳號是否可用
if(StringUtils.isNotBlank(state) && "1".equals(state) && StringUtils.isNotBlank(enable) && "1".equals(enable)){
return true;
}
return false;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof User) {
return getEmail().equals(((User)obj).getEmail())||getUsername().equals(((User)obj).getUsername());
}
return false;
}
@Override
public int hashCode() {
return getUsername().hashCode();
}
}
注: User 類修改後,對應功能的 Service 也得修改,因爲增加了 List<Role> 屬性,所以需要進行聯表查詢。
實現 UserDetailsService 接口,重寫 loadUserByUsername 方法:
public class AccountDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userService.findByEmail(email);//根據 email 獲取用戶
if(user == null){
throw new UsernameNotFoundException("用戶名或密碼錯誤");
}
List<Role> roles = roleService.findByUid(user.getId());
user.setRoles(roles);//設置用戶的的角色屬性
return user;
}
}
如果我們認證邏輯還需要加上驗證碼,則需要修改認證邏輯過濾器: 繼承 UsernamePasswordAuthenticationFilter 重寫 attemptAuthentication 方法。
public class AccountAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private String codeParameter = "code";
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = this.obtainUsername(request);//用戶名
String password = this.obtainPassword(request);//密碼
String code = request.getParameter(this.codeParameter);//驗證碼
String caChecode = (String)request.getSession().getAttribute("VERCODE_KEY");
boolean flag = CodeValidate.validateCode(code,caChecode);//校驗驗證碼
if(!flag){
throw new UsernameNotFoundException("驗證碼錯誤");
}
if(username == null) {
username = "";
}
if(password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);//token
this.setDetails(request, authRequest);//設置請求信息
//通過 AuthenticationManager 找到支持的 AuthenticationProvider 進行認證
return this.getAuthenticationManager().authenticate(authRequest);
}
}
此外需要設置訪問失敗,即權限不夠的跳轉頁面: 實現 AccessDeniedHandler 接口,重寫 handle 方法
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private String errorPage;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//根據請求頭中 X-Requested-With 的屬性值是否是 XMLHttpRequest 來判斷是不是 AJAX 請求。
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
//如果是 AJAX 請求則返回 JSON 格式數據,並結束方法。
if (isAjax) {
String jsonObject = "{\"message\":\"Access is denied!\",\"access-denied\":true}";
String contentType = "application/json";
response.setContentType(contentType);
PrintWriter out = response.getWriter();
out.print(jsonObject);
out.flush();
out.close();
return;
} else {
//如果不是 AJAX 請求。再判斷 errorPage 是否爲空,如果不爲空,則設置狀態碼爲403,並轉發到配置的錯誤頁面,如果 errorPage 爲空,則直接返回403錯誤頁面
if (!response.isCommitted()) {
if (this.errorPage != null) {
request.setAttribute("SPRING_SECURITY_403_EXCEPTION", e);
response.setStatus(403);
RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPage);
dispatcher.forward(request, response);
} else {
response.sendError(403, e.getMessage());
}
}
}
}
public void setErrorPage(String errorPage) {//獲取配置文件中 errorPage 的路徑
if(errorPage != null && !errorPage.startsWith("/")) {
throw new IllegalArgumentException("errorPage must begin with '/'");
} else {
this.errorPage = errorPage;
}
}
}
使用時還需要記得在 web.xml 加載 sercurity 配置文件,並配置權限過濾器鏈:
<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>
而在我們需要獲取當前用戶時,可以這樣獲取:
public User getCurrentUser(){
User user = null;
Authentication authentication = null;
SecurityContext context = SecurityContextHolder.getContext();
if(context!=null){
authentication = context.getAuthentication();
}
if(authentication!=null){
Object principal = authentication.getPrincipal();
//如果是匿名用戶
if(authentication.getPrincipal().toString().equals( "anonymousUser" )){
return null;
}else {
user = (User)principal;
}
}
return user;
}
最後貼上 sercurity 的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 靜態資源、登錄等 不攔截 -->
<security:http security="none" pattern="/css/**" />
<security:http security="none" pattern="/js/**" />
<security:http security="none" pattern="/images/**" />
<security:http security="none" pattern="/favicon.ico"/>
<security:http security="none" pattern="/login*" />
<security:http security="none" pattern="/checkCode"/>
<security:http security="none" pattern="/checkEmail"/>
<!--
配置具體的規則
auto-config="true" 不用自己編寫登錄的頁面,框架提供默認登錄頁面
use-expressions="false" 是否使用SPEL表達式
-->
<security:http auto-config="false" access-decision-manager-ref="accessDecisionManager"
use-expressions="true" entry-point-ref="loginEntryPoint">
<security:headers>
<security:frame-options disabled="true"></security:frame-options>
</security:headers>
<!-- 配置頁面信息 -->
<security:form-login login-page="/login" authentication-failure-url="/login?error=1"
login-processing-url="/doLogin" password-parameter="password"
default-target-url="/list"
username-parameter="username" />
<!-- 將 frame-options 設置爲禁用,否則瀏覽器拒絕當前頁面加載任何 Frame 頁面。如果不加如下設置,上傳圖片時會超時: -->
<security:access-denied-handler ref="accessDeniedHandler" />
<!-- 關閉跨域請求 -->
<security:csrf disabled="true"/>
<!-- 配置具體的攔截的規則 pattern="請求路徑的規則" access="訪問系統的人,必須有ROLE_USER的角色" -->
<security:intercept-url pattern="/" access="permitAll"/>
<security:intercept-url pattern="/index**" access="permitAll"/>
<security:intercept-url pattern="/sendSms" access="permitAll"/>
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
<!-- session失效url session策略-->
<security:session-management invalid-session-url="/index.jsp" session-authentication-strategy-ref="sessionStrategy">
</security:session-management>
<!-- spring-security提供的過濾器 以及我們自定義的過濾器 authenticationFilter-->
<security:custom-filter ref="logoutFilter" position="LOGOUT_FILTER" />
<security:custom-filter before="FORM_LOGIN_FILTER" ref="authenticationFilter"/>
<security:custom-filter after="FORM_LOGIN_FILTER" ref="phoneAuthenticationFilter"/>
<security:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/>
</security:http>
<!-- 我們的 MyAccessDeniedHandler -->
<bean id="accessDeniedHandler"
class="moke.demo.ssm.security.account.MyAccessDeniedHandler">
<property name="errorPage" value="/accessDenied.jsp" />
</bean>
<!-- 認證管理器,使用自定義的 UserService,並對密碼採用md5加密 -->
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider user-service-ref="accountService">
<security:password-encoder hash="md5">
<security:salt-source user-property="username"></security:salt-source>
</security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<!-- 自定義的過濾器 AccountAuthenticationFilter
登錄URL、認證管理器、Session策略、認證成功處理器和認證失敗處理器
-->
<bean id="authenticationFilter" class="moke.demo.ssm.security.account.AccountAuthenticationFilter">
<property name="filterProcessesUrl" value="/doLogin"></property>
<property name="authenticationManager" ref="authenticationManager"></property>
<property name="sessionAuthenticationStrategy" ref="sessionStrategy"></property>
<property name="authenticationSuccessHandler">
<bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/list"></property>
</bean>
</property>
</bean>
<!-- 配置登出後的處理 -->
<bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<!-- 處理退出的虛擬url -->
<property name="filterProcessesUrl" value="/loginout" />
<!-- 退出處理成功後的默認顯示url -->
<constructor-arg index="0" value="/login?logout" />
<constructor-arg index="1">
<!-- 退出成功後的handler列表 -->
<array>
<bean id="securityContextLogoutHandler"
class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" />
</array>
</constructor-arg>
</bean>
<!-- Session策略 -->
<bean id="sessionStrategy" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<constructor-arg>
<list>
<bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<property name="maximumSessions" value="1"></property>
<property name="exceptionIfMaximumExceeded" value="false"></property>
<constructor-arg ref="sessionRegistry"/>
</bean>
<bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy"/>
<bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
<constructor-arg ref="sessionRegistry"/>
</bean>
</list>
</constructor-arg>
</bean>
<!-- ConcurrentSessionFilter過濾器配置(主要設置賬戶session過期路徑) -->
<bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter">
<constructor-arg ref="sessionRegistry"></constructor-arg>
<constructor-arg value="/login?error=expired"></constructor-arg>
</bean>
<!-- 判斷是否過期以及刷新最後一次方法時間 -->
<bean id="sessionRegistry" scope="singleton" class="org.springframework.security.core.session.SessionRegistryImpl"></bean>
<bean id="accountService" class="moke.demo.ssm.security.account.AccountDetailsService"/>