【JavaEE】SSH+Spring Security自定義Security的部分處理策略


本文建立在 SSH與Spring Security整合 一文的基礎上,從這篇文章的example上做修改,或者從 配置了AOP 的example上做修改皆可。這里主要補充我在實際使用Spring Security中常用的一些前文最基本example中沒能提供的功能,主要包括自定義403錯誤頁面自定義認證管理器的內容提供者自定義登錄成功的回調接口自定義json訪問時未登錄和403錯誤的返回內容用代碼模擬Spring Security的驗證

1. 自定義權限不夠時訪問的界面

在搭建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>

再次訪問受限的資源就會跳轉到這個界面上。

2. 自己定義認證管理器的內容提供者

先回顧一下前文中怎么做用戶名密碼驗證的:

<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>

3. 自定義登錄成功之后的回調

有時候,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" />

4. 自定義json訪問時未登錄和403錯誤的返回內容

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串就可以了。

5. 代碼模擬登錄

前面的介紹都是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里,最后幾行做的就是這個事情。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM