死灰復燃的Security

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

  1. 基礎使用web

  2. 與OAuth2.x的集成spring

 

1. Security的基礎使用

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

1.1 SpringBoot中Security的默認配置

咱們建立一個SpringBoot項目,而後引入spring-security,pom中的依賴以下所示:json

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

而後咱們添加一個測試的接口,以下所示:api

@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);
    }
}

咱們啓動,就會發如今日誌裏,他給咱們生成了這樣的一段內容:tomcat

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官方說再也不提供認證服務器,可是以前的仍然是可使用的,只是在未來會被移除.

本文相關視頻

相關文章
相關標籤/搜索