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

上一篇我們簡單的分析了一下認證流程,通過程序的啟動加載了各類的配置信息。接下來我們一起來看一下授權流程,爭取完成和前面簡單的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中沒辦法同時設置多個用戶,認證成功後我想跳轉到自定義的頁面或者自定義的信息。別急,從下一篇開始,我們將逐步對代碼進行改造,一步一步打造成你想滿足的各種需求

(完)

Tags: