Spring security 總結

在之前的一個項目中有使用到,方便以後項目框架的搭建,這裏還是再總結一下。


Spring Security 的認證流程

在這裏插入圖片描述

  1. 用戶發出登錄請求。

  2. 首先經過 SecurityContextPersistenceFilter 過濾器,將 Session 中的認證信息保存到 SecurityContextHodlder 中

  3. 然後在認證邏輯過濾器UsernamePasswordAuthenticationFilter,封裝令牌 Token,設置請求信息等。一般我們需要自定義認證邏輯,所以需要繼承並重寫

  4. 然後通過 AuthenticationManager 認證管理器,遍歷所有的 AuthenticationProvider,找到支持該 Token 的認證提供者即 AbstractUserDetailsAuthenticationProvide。

  5. AbstractUserDetailsAuthenticationProvider 會調用它的子類 DaoAuthenticationProvider 的 retrieveUser 方法來獲取用戶信息 UserDetails。

  6. 而 DaoAuthenticationProvider 會調用 UserDetailsService 接口的 loadUserByUsername 方法來獲取用戶信息 UserDetails,我們只要實現 UserDetailsService 接口,寫獲取用戶信息的邏輯就可以了

  7. 如果整個過程都沒有異常,則認證通過,最終將認證結果 Authentication 保存到 SecurityContext 中,然後將 SecurityContext 保存到 SecurityContextHolder 中。

  8. 再次經過 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"/>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章