死灰復燃的Security 頂 原

一直以來,Spring系列給人的感覺都是快速,簡潔,好理解,易操作.但Security是一個特例,這個框架相比而言,首先就是複雜,其次是靈活性也不夠.好在於是Spring出的,因此與Spring配合比較好.並且在Spring的大力推廣和支持下,它仍然屹立在這裏.當然它也有自己的優點,比如他與LDAP還有Oauth這些結構的集成,處理的也不錯.我們今天主要從以下幾個方面來分享關於Security的知識:

  1. 基礎使用

  2. 與OAuth2.x的集成

 

1. Security的基礎使用

在web應用的設計中,權限是一個繞不開的話題.而在web權限設計中,RBAC是最流行的設計思路了.(除了RABC,還有像Linux中的ACL權限設計).在RBAC這種設計思路的引導下,我們可以有很多種實現方式,從最簡單的一個過濾器開始,到Security或Shiro,甚至和其他的第三方進行集成,都是沒有問題的.今天我們就先來看看Spring Security怎麼使用.

1.1 SpringBoot中Security的默認配置

我們創建一個SpringBoot項目,然後引入spring-security,pom中的依賴如下所示:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

</dependencies>

然後我們添加一個測試的接口,如下所示:

@RestController
@RequestMapping("users/")
public class UserInfoController {

    @GetMapping("hello")
    public String hello(){
        return "HelloWorld";
    }
}

最後是我們的啓動類,其實啓動類並沒有任何改變:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

我們啓動,就會發現在日誌裏,他給我們生成了這樣的一段內容:

2018-11-13 13:42:15.307  INFO 13084 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2018-11-13 13:42:15.620  INFO 13084 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: b74fd02a-0ad2-40ec-b6cd-3f2edfa015c1

2018-11-13 13:42:15.756  INFO 13084 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7f13811b, org.springframework.security.web.context.SecurityContextPersistenceFilter@22d7fd41, org.springframework.security.web.header.HeaderWriterFilter@4fc165f6, org.springframework.security.web.csrf.CsrfFilter@65514add, org.springframework.security.web.authentication.logout.LogoutFilter@3bc69ce9, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1ca610a0, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@79980d8d, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@59fc6d05, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@1775c4e7, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@19fd43da, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@2785db06, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@78307a56, org.springframework.security.web.session.SessionManagementFilter@5a7df831, org.springframework.security.web.access.ExceptionTranslationFilter@750f64fe, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1f9d4b0e]
2018-11-13 13:42:15.878  INFO 13084 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2018-11-13 13:42:15.885  INFO 13084 --- [           main] top.lianmengtu.security.Application      : Started Application in 4.07 seconds (JVM running for 4.73)

這裏給我們生成了一個密碼,它是做什麼的呢?我們現在來訪問我們的測試接口,然後就會有一個登錄窗口讓你登錄.這是怎麼回事兒呢?

http://localhost:8080/users/hello

這是因爲當我們添加了security模塊後,SpringBoot默認爲我們啓用了security的攔截,並且如果我們沒有配置默認的用戶名密碼的話,他就給我們生成了一個默認的用戶名user,而密碼則就是我在上面的日誌中.當我們完成登錄後,我們就可以正常使用我們的接口了.

1.2 SpringBoot中SpringSecurity的簡易配置

現在我們來對SpringSecurity進行自定義用戶名密碼配置,我們創建一個application.yml,然後設置如下:

 

spring:
  security:
    user:
      name: zhangsan
      password: zhangsan123

然後重啓我們的應用,我們會發現,SpringBoot不在給我們提供默認密碼了,而當我們訪問我們的接口的時候,我們可以使用新配置的zhangsan和zhangsan123進行登錄.這樣的配置主要是由SpringSecurity中的WebSecurityConfig來實現的,因此我們也可以將用戶名和密碼寫到那裏面,這裏就不給大家演示了.

1.3 Security的一些自定義實現

但這種方式仍然不夠靈活,通常我們都會考慮由我們自己來定義用戶信息以及權限信息,其實用戶信息與權限信息也是權限框架關注的兩個主要點,這兩點也被稱爲認證及授權.所謂認證,通俗點來講,就是登錄校驗,確定訪問用戶的憑據是否正確.所謂授權就是該合法用戶是否擁有對應的權限.

這是我們就需要問一個問題,Spring Security如何處理url與權限的匹配,也就是說Spring Security他如何知道哪些url是可以被公開訪問,哪些url登錄後可以訪問,哪些還需要某些固定的權限纔可以訪問?

1.3.1 用戶認證

這些問題的答案就在WebSecurityConfigurerAdapter裏,在這個Adapter裏有兩個configure函數,一個是configure(HttpSecurity http),主要作用是配置哪些url可以直接放過,哪些是需要登錄才能訪問的,另外一個是configure(AuthenticationManagerBuilder auth),這個函數主要是用來做用戶認證的,我們現在先來寫url的映射與攔截.如下所示:

package top.lianmengtu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
    }
    
}

此時如果我們訪問我們的接口,就會出現403的場景,如下所示,有沒有很熟悉的感覺:

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Nov 14 17:58:11 CST 2018
There was an unexpected error (type=Forbidden, status=403).
Access Denied

只是這樣還不行,因爲我們希望能夠放過一些接口,比如登錄,然後其他的希望讓用戶登錄之後能夠進行訪問.我們先來改造一下我們的url處理接口configure(HttpSecurity http).

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/auth/login").permitAll()//放過登錄接口
            .and().formLogin().loginProcessingUrl("/auth/login")//指定登錄處理接口
            .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登錄成功與登錄失敗的處理器
            .and().authorizeRequests().anyRequest().authenticated()//對其他接口的權限限制爲登錄後才能訪問
            .and().csrf().disable();//禁用csrf攔截,如果使用restclient和postman測試,建議禁掉,要不然會出錯
}

此時就需要用到configure(AuthenticationManagerBuilder auth)這個函數了.我們可以透過這個函數注入一個UsersDetailsService,然後在UserDetailsService裏來進行準確的處理.此時完成的SecurityConfig如下所示:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import top.lianmengtu.security.handler.LoginFailHandler;
import top.lianmengtu.security.handler.LoginSuccessHandler;

/**
 * @program test_security
 * @description
 * @author: Jacob.Li
 * @create: 2018-11-13 16:01
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailServiceImpl userDetailService;

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailHandler loginFailHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/auth/login").permitAll()//放過登錄接口
                .and().formLogin().loginProcessingUrl("/auth/login")//指定登錄處理接口
                .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登錄成功與登錄失敗的處理器
                .and().authorizeRequests().anyRequest().authenticated()//對其他接口的權限限制爲登錄後才能訪問
                .and().csrf().disable();//禁用csrf攔截,如果使用restclient和postman測試,建議禁掉,要不然會出錯
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

UserDetailsService的實現如下:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import top.lianmengtu.security.users.model.UserInfo;
import top.lianmengtu.security.users.service.IUserInfoService;


@Component
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private IUserInfoService userInfoService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("----Hello");
        System.out.println("name:"+username);
        if(!username.equals("zhangsan")){
            throw new UsernameNotFoundException("用戶名不對");
        }
        UserInfo userInfo=userInfoService.loadByNickName(username);
        return User.withUsername(username).password(userInfo.getPassword()).roles("ADMIN").build();
    }
}

UserDetailsService裏有一個userInfoService,這個是我們臨時自定義的一個接口,我們可以在這個接口裏接入數據庫,這裏只是一個簡單的實現,如下所示:

package top.lianmengtu.security.users.service;

import top.lianmengtu.security.users.model.UserInfo;

public interface IUserInfoService {
    public UserInfo loadByNickName(String nickName);
}


package top.lianmengtu.security.users.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import top.lianmengtu.security.users.model.UserInfo;
import top.lianmengtu.security.users.service.IUserInfoService;


@Service
public class UserInfoService implements IUserInfoService {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserInfo loadByNickName(String nickName) {
        UserInfo userInfo=new UserInfo();
        userInfo.setNickName("zhangsan");
        userInfo.setPassword(passwordEncoder.encode("123456"));
        return userInfo;
    }
}

那登錄成功或失敗的處理邏輯呢?我們在SecurityConfig裏添加了LoginSuccessHandler和LoginFailHandler,也只是一個簡單的實現,這裏僅供參考:

package top.lianmengtu.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @program test_security
 * @description
 * @author: Jacob.Li
 * @create: 2018-11-14 16:41
 **/
@Component
public class LoginFailHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        ObjectMapper objectMapper=new ObjectMapper();
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        Map<String,String> result=new HashMap<>();
        result.put("code","-1");
        result.put("msg","用戶名/密碼錯誤,請重新登錄");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));
    }
}
package top.lianmengtu.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @program test_security
 * @description
 * @author: Jacob.Li
 * @create: 2018-11-14 14:01
 **/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        ObjectMapper objectMapper=new ObjectMapper();
        httpServletResponse.setContentType("application/json;charset=UTF-8"); // 響應類型
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString("登錄驗證成功"));
        System.out.println("-----login successful:"+objectMapper.writeValueAsString(authentication.getDetails()));
    }
}

然後我們啓用restclient進行測試,就可以得到我們預期的結果了.

1.3.2 用戶授權

現在我們完成了用戶的認證,那授權如何處理呢?其實在我們剛剛所展示出來的UserDetailsService裏,有一個roles,這裏描述的是用戶的角色,我們現在只需要做兩件事就可以了.第一件就是獲取當前請求的url及其需要的角色,第二件就是與當前用戶的角色進行比較並作出放行或者攔阻的操作.

在SpringSecurity中,進行url攔截的是FilterInvocationSecurityMetadataSource,這裏我們自定義一個MyFilterInvocationSecurityMetadataSource,主要用於拿到當前url所需要的角色信息,並將這個url對應的角色信息傳入到下一個組件AccessDecisionManager中然後與用戶所擁有的角色進行比較,如果url沒有找到或者url沒有角色信息,這裏添加了一個默認的登錄認證,也可以直接放行.代碼如下所示:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import top.lianmengtu.security.users.service.IAuthorityService;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
 * @program test_security
 * @description
 * @author: Jacob.Li
 * @create: 2018-11-15 09:37
 **/
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private IAuthorityService authorityService; //接入自定義的Service

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl=((FilterInvocation)object).getRequestUrl();
        System.out.println("----->MyFilterInvocationSecurityMetadataSource:拿到了url:"+requestUrl);
        if(requestUrl.equals("/auth/login")){ //登錄接口,直接放過
            return null;
        }
        List<String> roleList=authorityService.findRolesByUrl(requestUrl);
        if(roleList.size()>0){
            String[] roleArray=new String[roleList.size()];
            for (int i = 0; i < roleList.size(); i++) {
                roleArray[i]=roleList.get(i);
            }
            return SecurityConfig.createList(roleArray);
        }
        return SecurityConfig.createList("ROLE_LOGIN"); //其他接口設置爲登錄放行,也可以像登錄接口一樣直接放過
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

在這個實現中我們注入了我們自己的AuthorityService,這裏實現的比較簡單,大家可以根據需要從自己的數據庫裏對數據進行查找,Service示例如下所示:

package top.lianmengtu.security.users.service;

import java.util.List;

public interface IAuthorityService {

    public List<String> findRolesByUrl(String url);

}

-------------------------------------------------------

package top.lianmengtu.security.users.service.impl;

import org.springframework.stereotype.Service;
import top.lianmengtu.security.users.service.IAuthorityService;

import java.util.ArrayList;
import java.util.List;


@Service
public class AuthorityServiceImpl implements IAuthorityService {

    @Override
    public List<String> findRolesByUrl(String url) {
        System.out.println("-----url:"+url);
        List<String> rolesList=new ArrayList<>();
        rolesList.add("ADMIN");
        rolesList.add("MANAGER");
        return rolesList;
    }
}

當FilterInvocationSecurityMetadataSource的操作完成之後,他會將這個角色列表傳入到AccessDecisionManager中,在其中與登錄用戶的角色進行比較,然後決定放過還是攔截,自定義AccessDecisionManager代碼如下所示:

package top.lianmengtu.security.config;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Iterator;

/**
 * @program test_security
 * @description
 * @author: Jacob.Li
 * @create: 2018-11-15 09:55
 **/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        Iterator<ConfigAttribute> configAttributeIterator = configAttributes.iterator(); //獲取上個組件中傳過來的角色集合
        while (configAttributeIterator.hasNext()){
            ConfigAttribute configAttribute=configAttributeIterator.next();
            String role=configAttribute.getAttribute();
            if("ROLE_LOGIN".equals(role)){ //判斷是否需要具備登錄權限
                if(authentication instanceof AnonymousAuthenticationToken){
                    throw new BadCredentialsException("未登陸");
                }
                return;
            }
            Collection<? extends GrantedAuthority> currentUserAuthorities=authentication.getAuthorities();//獲取用戶的角色信息
            for(GrantedAuthority grantedAuthority: currentUserAuthorities){
                if(grantedAuthority.getAuthority().equals(role)){//如果用戶的角色信息包含了當前鏈接所需要的角色,則放行
                    return;
                }
            }
        }
        throw new AccessDeniedException("權限不足");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

現在我們已經做好了用戶授權的操作,現在是以異常的形式來展現結果,我們可以對結果進行處理,將結果轉換爲我們期望的json形式然後返回給前臺,這是由AccessDeniedHandler來處理的,自定義AccessDeniedHandler的代碼如下所示:

package top.lianmengtu.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * @program test_security
 * @description
 * @author: Jacob.Li
 * @create: 2018-11-15 10:06
 **/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        httpServletResponse.setCharacterEncoding("UTF-8");
        PrintWriter out = httpServletResponse.getWriter();
        Map<String,String> result=new HashMap<>();
        result.put("code","-1");
        result.put("msg","權限不足");
        ObjectMapper objectMapper=new ObjectMapper();
        String resultString=objectMapper.writeValueAsString(result);
        out.write(resultString);
        out.flush();
        out.close();
    }
}

截止到現在,我們的準備工作已經做完了,現在我們來進行最後一步整合的操作,我們修改一下我們自定義的那個WebSecurityConfigurerAdapter組件中的configure(HttpSecurity http)函數,來使我們的處理真正生效,代碼如下所示:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import top.lianmengtu.security.handler.LoginFailHandler;
import top.lianmengtu.security.handler.LoginSuccessHandler;
import top.lianmengtu.security.handler.MyAccessDeniedHandler;

/**
 * @program test_security
 * @description
 * @author: Jacob.Li
 * @create: 2018-11-13 16:01
 **/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailServiceImpl userDetailService;

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailHandler loginFailHandler;

    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Autowired
    private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;

    @Autowired
    private MyAccessDecisionManager myAccessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {//這裏的ObjectPostProcessor可以完全挪出去,像其他的handler一樣
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);//配置我們剛剛自定義好的FilterInvocationSecurityMetadataSource,來加載url所需要的角色
                        object.setAccessDecisionManager(myAccessDecisionManager);//配置我們剛剛自定義好的AccessDecisionManager,來進行用戶角色和url所需角色的對比
                        return object;
                    }
                })
                .antMatchers("/auth/login").permitAll()//放過登錄接口
                .and().formLogin().loginProcessingUrl("/auth/login")//指定登錄處理接口
                .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登錄成功與登錄失敗的處理器
                .and().authorizeRequests().anyRequest().authenticated()//對其他接口的權限限制爲登錄後才能訪問
                .and().csrf().disable()
                .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);//授權失敗的處理
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

好了,我們的授權工作已經完成了,現在我們可以重新啓動我們的項目,然後可以用restClient訪問我們的接口進行測試了.

2. OAuth2.0

我們可以看到,基本授權使用Spring Security是比較複雜的,我們完全可以只用一個Filter加幾個自定義註解完成這項工作,這也是很多人在開發一個單體項目時所做的一件事兒.這裏之所以提到單體項目是因爲在多個項目之間,如果我們想要進行權限限制,就不能在這麼做了,尤其是我們將我們的部分資源公開提供給其他人使用的時候,這樣的一刀切權限可能就不太適合.

當多個應用之間共享權限的時候,我們往往會關注兩個問題,第一是權限的安全性,第二,是權限的粒度.什麼意思呢?比如說我們提供了一個資源管理服務器,裏面有照片、視頻、文檔,然後大家可以往我們的資源服務器上傳自己的資源.後來用戶又在另外一個網站上需要上傳自己的頭像,但他不想用默認的頭像,想用自己在資源服務器上的照片,那麼此時,如果他將自己在資源服務器上的用戶名和密碼告訴那個網站的話,首先是不安全,那個網站可以隨時訪問他的資源服務器,其次是權限太大,第三方網站不僅僅可以訪問他的照片,還能夠訪問他的視頻和文檔,這就很危險了.爲了解決這類問題,OAuth就誕生了.他允許用戶進行部分授權.他的原理很簡單.

OAuth在資源服務器和第三方網站之間做了一個授權層,然後授權層負責針對客戶端進行權限的限制,包括權限的範圍,有效期,這樣客戶端因爲無法直接訪問資源服務器,從而保護了用戶的資源.

那麼他的運行流程呢,就是這樣的:

  1. 用戶打開客戶端以後,客戶端要求用戶給予授權。

  2. 用戶同意給予客戶端授權。

  3. 客戶端使用上一步獲得的授權,向認證服務器申請令牌。

  4. 認證服務器對客戶端進行認證以後,確認無誤,同意發放令牌。

  5. 客戶端使用令牌,向資源服務器申請獲取資源。

  6. 資源服務器確認令牌無誤,同意向客戶端開放資源。

2.1 OAuth2.0授權模式

在以上這6步中,最關鍵的一步就是2,用戶如何才能夠給客戶端授權呢?OAuth2.0提供了四中授權方式,分別爲:

  1. 授權碼模式

  2. 簡化模式

  3. 密碼模式

  4. 客戶端模式

2.1.1授權碼模式

授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。它的特點就是通過客戶端的後臺服務器,與"服務提供商"的認證服務器進行互動。他的具體步驟如下:

  1. 用戶訪問客戶端,後者將前者導向認證服務器。

  2. 用戶選擇是否給予客戶端授權。

  3. 假設用戶給予授權,認證服務器將用戶導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。

  4. 客戶端收到授權碼,附上早先的"重定向URI",向認證服務器申請令牌。這一步是在客戶端的後臺的服務器上完成的,對用戶不可見。

  5. 認證服務器覈對了授權碼和重定向URI,確認無誤後,向客戶端發送訪問令牌(access token)和更新令牌(refresh token)。

2.1.2 簡化模式

簡化模式(implicit grant type)不通過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"授權碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。步驟如下:

  1. 客戶端將用戶導向認證服務器。

  2. 用戶決定是否給於客戶端授權。

  3. 假設用戶給予授權,認證服務器將用戶導向客戶端指定的"重定向URI",並在URI的Hash部分包含了訪問令牌。

  4. 瀏覽器向資源服務器發出請求,其中不包括上一步收到的Hash值。

  5. 資源服務器返回一個網頁,其中包含的代碼可以獲取Hash值中的令牌。

  6. 瀏覽器執行上一步獲得的腳本,提取出令牌。

  7. 瀏覽器將令牌發給客戶端。

2.1.3 密碼模式

密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向"服務商提供商"索要授權。在這種模式中,用戶必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。這通常用在用戶對客戶端高度信任的情況下,比如客戶端是操作系統的一部分,或者由一個著名公司出品。而認證服務器只有在其他授權模式無法執行的情況下,才能考慮使用這種模式。步驟如下:

  1. 用戶向客戶端提供用戶名和密碼。

  2. 客戶端將用戶名和密碼發給認證服務器,向後者請求令牌。

  3. 認證服務器確認無誤後,向客戶端提供訪問令牌。

2.1.4 客戶端模式

客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以用戶的名義,向"服務提供商"進行認證。嚴格地說,客戶端模式並不屬於OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端註冊,客戶端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。步驟如下:

  1. 客戶端向認證服務器進行身份認證,並要求一個訪問令牌。

  2. 認證服務器確認無誤後,向客戶端提供訪問令牌。

2.2 SpringBoot中的OAuth2.0

現在我們知道OAuth授權主要由三部分構成,首先是客戶端,然後是認證服務器,最後是我們的資源服務器,之前SpringBoot一直將OAuth2.0認證放在了SpringSecurity下,但在SpringBoot2.0文檔中,Spring官方說不再提供認證服務器,但是之前的仍然是可以使用的,只是在將來會被移除.

本文相關視頻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章