【認證與授權】Spring Security的授權流程

黑米麵包派發表於2020-04-28

上一篇我們簡單的分析了一下認證流程,通過程式的啟動載入了各類的配置資訊。接下來我們一起來看一下授權流程,爭取完成和前面簡單的web基於sessin的認證方式一致。由於在授權過程中,我們預先會給用於設定角色,關於如果載入配置的角色資訊這裡就不做介紹了,上一篇的載入過程中我們可以發現相關的資訊。

本篇依舊基於spring-security-basic

配置角色資訊

配置使用者及其角色資訊的方式很多,我們這次依舊採取配置檔案的方式,不用程式碼或其他的配置方式,在之前的配置使用者資訊的地方application.yml,新增使用者的角色資訊。

spring:
  security:
    user:
      name: admin
      password: admin
      roles: ADMIN,USER

這樣我們就完成了最簡單的使用者角色賦予。在載入使用者資訊時我們知道會生成一個User物件,將其使用者名稱、密碼、許可權資訊封裝進去。

這裡需要注意一下關於role資訊的載入

public UserBuilder roles(String... roles) {
    List<GrantedAuthority> authorities = new ArrayList<>(
        roles.length);
    for (String role : roles) {
        Assert.isTrue(!role.startsWith("ROLE_"), () -> role
                      + " cannot start with ROLE_ (it is automatically added)");
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
    }
    return authorities(authorities);
}

也就是說我們上方配置的ADMIN,USER會被轉化成ROLE_ADMIN,ROLE_USER

1、獲取使用者資訊

我們在BasicController類中新增一個獲取認證使用者資訊的介面

@RequestMapping("/getUser")
public String api(HttpServletRequest request) {
    // 方式一
    Principal userPrincipal = request.getUserPrincipal();
    UsernamePasswordAuthenticationToken user = ((UsernamePasswordAuthenticationToken) userPrincipal);
    System.out.println(user.toString());
	// 方式二
    SecurityContext securityContext = SecurityContextHolder.getContext();
    System.out.println(securityContext.getAuthentication());
	// 方式三
    Object context = request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
    SecurityContext securityContext1 = (SecurityContext) context;
    System.out.println(securityContext1.getAuthentication());

    return user.toString();
}

我們從session中去獲取使用者的資訊,然後拿到其授權資訊就可以做相應的判斷了request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");這一段程式碼我們找到是在HttpSessionSecurityContextRepository.saveContext(SecurityContext context)中放入的,SPRING_SECURITY_CONTEXT是其維護的常量,這樣我們就有可以根據這個key去獲取當前的會話資訊了。

當然我們還有另外的獲取使用者資訊的方式還記得我們在AbstractAuthenticationProcessingFilter這個核心過濾器中的successfulAuthentication方法

protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response, FilterChain chain, Authentication authResult)
    throws IOException, ServletException {

    if (logger.isDebugEnabled()) {
        logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                     + authResult);
    }

    SecurityContextHolder.getContext().setAuthentication(authResult);

    rememberMeServices.loginSuccess(request, response, authResult);

    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
            authResult, this.getClass()));
    }

    successHandler.onAuthenticationSuccess(request, response, authResult);
}

這裡將其認證成功的結果資訊放入到上下文中 SecurityContextHolder.getContext().setAuthentication(authResult);那我們也是可以直接通過其get方法獲取SecurityContextHolder.getContext();

登陸後直接訪問介面localhost:8080/getUser

org.springframework.security.authentication.UsernamePasswordAuthenticationToken@bade0105: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffbcba8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: E4C77C8791C314B7B14F796B0DD38F13; Granted Authorities: ROLE_ADMIN
org.springframework.security.authentication.UsernamePasswordAuthenticationToken@bade0105: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffbcba8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: E4C77C8791C314B7B14F796B0DD38F13; Granted Authorities: ROLE_ADMIN
org.springframework.security.authentication.UsernamePasswordAuthenticationToken@bade0105: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffbcba8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: E4C77C8791C314B7B14F796B0DD38F13; Granted Authorities: ROLE_ADMIN

可以看到,控制檯列印的三段資訊是完全一樣的。說明這裡通過三種方式獲取的使用者資訊是一致的。既然可以獲取到當前登入的使用者資訊,接下來我們就可以通過使用者資訊的判斷來決定其是否可以訪問那些介面。

2、自定義攔截器

上一步我們通過三種方式獲取到了認證使用者的資訊,這裡我們將設計一個攔截器來控制使用者的訪問許可權。我們先設計兩個介面,一個只能admin角色使用者才可以訪問,一個只能user角色使用者才可以訪問

@RequestMapping("/api/admin")
public String adminApi(HttpServletRequest request){
    Principal principal = request.getUserPrincipal();
    String name = principal.getName();
    return "管理員:" + name + "你好,你可以訪問/api/admin";
}

@RequestMapping("/api/user")
public String userApi(HttpServletRequest request){
    Principal principal = request.getUserPrincipal();
    String name = principal.getName();
    return "普通使用者:" + name + "你好,你可以訪問/api/user";
}

我們設計了兩個介面,通過url來區別不同角色訪問的結果,我們再設計一個攔截器,這裡我們可以直接參考前面的文章 基於session的認證方式 中定義的攔截器

public class AuthenticationInterceptor extends HandlerInterceptorAdapter {
    private final static String USER_SESSION_KEY = "SPRING_SECURITY_CONTEXT";
    // 前置攔截,在介面訪問前處理
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object attribute = request.getSession().getAttribute(USER_SESSION_KEY);
        if (attribute == null) {
            writeContent(response,"匿名使用者不可訪問");
            return false;
        } else {
            SecurityContext context = (SecurityContext) attribute;
            Collection<? extends GrantedAuthority> authorities = context.getAuthentication().getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals("ROLE_ADMIN") && request.getRequestURI().contains("admin")){
                    return true;
                }
                if (authority.getAuthority().equals("ROLE_USER") && request.getRequestURI().contains("user")){
                    return true;
                }
            }
            writeContent(response,"許可權不足");
            return false;
        }
    }
    //響應輸出
    private void writeContent(HttpServletResponse response, String msg) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf‐8");
        PrintWriter writer = response.getWriter();
        writer.write(msg);
    }
}

同時生效該攔截器

@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
    // 新增自定義攔截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/api/**");
    }
}

3、註解方式判斷

通過攔截器的方式配置,看上去非常的繁瑣,如果我需要給某個介面新增一個角色訪問許可權,還需要去修改攔截器中的判斷邏輯。當然Spring Security也提供了非常方便的註解模式去控制介面,需要修改哪個介面的角色訪問,直接在介面上修改就可以了

@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/api2/admin")
public String admin2Api(String message){
    return "hello : " + message;
}

@PreAuthorize("hasRole('USER')")
@RequestMapping("/api2/user")
public String user2Api(String message){
    return "hello : " + message;
}

非常的簡單,一個註解就幫我們解決了攔截器中完成的事情,其實他們的原理是差不多的。不過這裡有幾個需要關注的點

  • @PreAuthorize註解的生效,需要提前開啟的。需要在@EnableGlobalMethodSecurity(prePostEnabled = true) 註解中生效,因為PreAuthorize 預設是false

  • @PreAuthorize是支援表示式方式進行設定的,我用的是hasRole。是其內建的表示式庫SecurityExpressionRoot中的方法

  • hasRole最終呼叫的是hasAnyAuthorityName的方法,這裡會有一個預設的字首,當前你也可以寫成hasRole('ROLE_ADMIN')的。並且是變長陣列,我們還可一進行多角色的判斷例如:hasRole('ROLE','USER')

    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = getAuthoritySet();
    
        for (String role : roles) {
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }
    
        return false;
    }
    

到這裡,我們已經完成了基於攔截器和註解方式的介面授權設定,基本上都是在零配置的基礎上完成的。我們寫發現了,好像不太容易擴充套件資訊,例如application.yml中沒辦法同時設定多個使用者,認證成功後我想跳轉到自定義的頁面或者自定義的資訊。別急,從下一篇開始,我們將逐步對程式碼進行改造,一步一步打造成你想滿足的各種需求

(完)

相關文章