31.SpringSecurity-使用JWT實現SSO效果

前言

什麼是單點登陸?
咱們看一個例子:咱們訪問taobao的時候,在點擊下淘寶主頁的天貓,咱們發現其實他是兩個域名;因此應該是不一樣的服務器。 html

image.png
而後咱們再次登陸下淘寶帳號: 前端

image.png
淘寶登陸的域名又是和淘寶/天貓首頁的不一致,三個不一樣的域名錶明瞭3個不一樣服務器。 java

而後咱們輸入帳號登陸淘寶,而後刷新下天貓,發現天貓也登錄了。web

單點登陸:咱們登陸一臺服務器(系統)以後,同時也登陸了另外一臺服務(系統)。spring

內容

1. JWT實現SSO的流程

基於JWT實現SSO登陸。數據庫

  1. 咱們有三臺服務器:應用A、應用B、認證服務器;類比於咱們的淘寶(應用A)、天貓(應用B)、淘寶登陸頁(認證服務器)。

image.png

  1. 當一個訪問請求發給咱們應用A時候,若是這個請求只有在登陸之後才能訪問,那麼應用A就會向認證服務器請求受權(把用戶引導到認證服務器上)。那麼此時用戶在認證服務器上進行認證並受權。注意用戶在作登陸動做是在認證服務器上作的。用戶一旦認證/受權以後就會返回受權碼給應用A,那麼應用A拿着這個受權碼去請求令牌,認證服務器給應用A返回JWT。應用A解析這裏面信息,而後並登陸。此時用戶就在應用A上登陸。 在第二步流程走完以後,咱們返回的JWT實際上是包含當前用戶在認證服務器上進行登陸之後所登陸的用戶信息都是放在JWT信息裏面給返回回去的。那麼這個應用A他解析裏面JWT信息,而後用這些信息生成自個認證的Authentication放到他的Spring Security的Security Context裏面,這樣的話它至關於利用這個用戶的這些信息在他本身的網站上進行了登陸,那麼後續的請求能夠直接訪問應用A上的資源了。

image.png

  1. 當他在應用A上完成了這個登陸之後,他跳到應用B上的時候,(這種跳不論是開一個新頁面,或者輸一個新的地址也好,或者連接過去也好),對於應用B來講,仍然是一個未受權的狀態,他也同樣須要在登陸時候請求認證服務器,讓認證服務器給他受權,讓用戶使其可以訪問應用B,這個時候,用戶會再次進行受權。可是他如今不用再次登陸了,由於第一次請求受權的時候,已經在認證服務器上登陸過了,那麼咱們再經過應用B瀏覽器跳過來的時候。實際上認證服務器知道當前用戶是誰的,他只是會邀請用戶去受權。你能夠用咱們當前的登陸信息去訪問這個應用B。而後後面的流程是同樣的,也會發送受權碼,而後再次獲取令牌,返回令牌給應用B(注意此時發給應用A和應用B的token是不同的,字符串是不同的,可是經過字符串解析出來的用戶信息是同樣的)。應用B拿到JWT以後,作本身的解析。會解析出同樣的用戶信息來,而後用這個同樣的用戶信息去構建他的Authentication.放到他的Spring Security Context裏面,完成他在應用B的登陸。最終的結果是什麼,最終的結果是:應用A的服務裏面的session裏面有一個security context,context裏面有Authentication。Authentication放的是用戶的信息。應用戶B的session裏面也有一個用戶信息,雖然這兩個用戶信息是從不一樣的JWT裏面解析出來的,可是解析出來的內容是同樣的。流程走到這裏,其實已經走完了sso的。以上用戶只在認證服務器上作了一次登陸,而後在應用A的session裏面和應用B的session裏面都會放入一樣的用戶信息。那麼這個用戶既能夠訪問應用A也能夠訪問應用B,用的身份是同樣的。

2. JWT實現SSO編碼

2.1 簡介

咱們寫的代碼是基於spring security,spring security oauth技術棧實現,上面JWT實現SSO的流程也是基於spring security,spring security oauth技術棧實現的描述的。可是若是你的應用A和應用B不是基於Spring Security來作的,甚至不是用java來寫的,上面的流程也是試用的。只須要應用A和應用B是基於http的,而後基於http完成上面流程你就能夠按照上面模式實現sso登陸。固然認證服務器和資源服務是須要咱們本身搭建的,可是搭建這些的話使用Spring Security是很容易實現的。express

2.2 搭建工程

咱們怎麼不在原來的代碼上去寫了,有2個緣由: 緣由一:原來代碼結構並不適合我當前sso登陸場景。原來代碼結構是按照瀏覽器安全session的安全 咱們須要怎樣控制?基於App的安全令牌token的方式咱們如何控制?,是按照上面2種方式來區分開的。 可是在sso的模式下,認證服務器是一個特殊存在,他是由基於瀏覽器的處理,各類跳轉,session的處理,另一部分,他也會發令牌。基於瀏覽器,基於session,同時也要發令牌。他會混合咱們以前講解的全部東西。 緣由二:在原來基礎上,咱們修改代碼是能夠實現sso的,可是代碼的複雜度會加大。 咱們單獨使用工程搭建,實現功能不會操做100行代碼,能夠把以前全部的代碼邏輯實現所有串起來。 後端

image.png

其中
oss-server:認證服務器
oss-clientA:應用服務A
oss-clientB:應用服務B瀏覽器

2.3 sso-server工程

  1. oss-server中添加依賴:

引入2個starter項目:web(會引入spring mvc 那套東西)、security(spring-security相關依賴)、咱們要基於oauth2發令牌,基於jwt生成令牌。安全

<dependencies>
        <!--spring security starter 依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--spring mvc starter web依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--spring-security-oauth2依賴-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>

        <!--spring-security-jwt依賴-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
        </dependency>
    </dependencies>

而後建立啓動類

  1. 配置標準認證服務器

a.配置client端,配置TokenConvert和TokeStore對應的bean,而後將其配置到configure的的endpoints去. b.認證服務器的安全配置:AuthorizationServerSecurityConfigurer

@Configuration
@EnableAuthorizationServer
public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    /**
     * 配置客戶端受權:
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("clientA")
                .secret("clientAsecret")
                .authorizedGrantTypes("authorization_code","refresh_token")
                .scopes("all")
                .and()
                .withClient("clientB")
                .secret("clientBsecret")
                .authorizedGrantTypes("authorization_code","refresh_token")
                .scopes("all");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(jwtTokenStore())
                .accessTokenConverter(jwtAccessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        /**
         * isAuthenticated():spring security的受權表達式。
         */
        //咱們訪問認證服務器的tokenKey時候須要通過身份認證;tokenKey就是咱們在jwtAccessTokenConverter裏面寫的yxm
        //咱們爲何須要訪問tokenKey?咱們以前sso認證流程時候,會生成一個jwt返回回去,而這個jwt是:須要祕鑰去簽名,咱們的場景裏面是:yxm
        //當應用A獲取到JWT時候,他解析裏面的東西 他就要去驗簽名  他要驗簽名 那麼這個應用A就須要知道簽名用的祕鑰是什麼?咱們後面讓應用A去訪問
        //tokenKey時候,就會受權才能獲取。
       security.tokenKeyAccess("isAuthenticated()");
    }

    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }


    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("yxm");//咱們將其寫死
        return converter;
    }
}
  1. 添加配置文件
    用戶認證的認證動做是在認證服務器上運行的,因此咱們須要在認證服務器上配置用戶信息。
    添加application.yml文件,裏面的內容是:
server:
  port: 9999
  context-path: /server
security:
  user:
    password: 123456

後面咱們從應用A跳轉到認證服務器時候輸入的密碼是要輸入:123456

2.4 sso-clientA工程

  1. 添加依賴:添加的依賴sso-server一致.
  2. 啓動類配置

    @SpringBootApplication
    @RestController
    @EnableOAuth2Sso //應用端SSO登陸須要添加此註解
    public class SsoClientApplication {
    
        @GetMapping("/user")
        public Object user(Authentication user){
            return user;
        }
    
        public static void main(String[] args) {
            SpringApplication.run(SsoClientApplication.class,args);
        }
    }
  3. 配置文件配置
    由於咱們做爲客戶端去訪問認證服務器。

    security:
      oauth2:
        client:
          clientId: clientA
          clientSecret: clientAsecret
          #配置應用A須要認證時候,認證服務器地址,應用跳轉進行認  證的url
          user-authorization-uri: http://127.0.0.1:9999/server/oauth/authorize
          #配置應用A須要認證完成以後,認證服務器返回的獲取token的地址
          access-token-uri: http://127.0.0.1:9999/server/oauth/token
        resource:
          jwt:
            # 用戶獲取到jwt後須要使用祕鑰解析jwt時候的祕鑰生成地址  
            key-uri: http://127.0.0.1:9999/server/oauth/token_key
    server:
      port: 7777
      context-path: /clientA
  4. 頁面配置
    頁面配置跳轉到clientB

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>SSO ClientA</title>
    </head>
    <body>
        <h1>SSO Demo ClientA</h1>
        <a href="http://127.0.0.1:8060/clientB/index.html">    訪問ClientB</a>
    </body>
     </html>

2.5 sso-clientB工程

  1. 添加依賴:添加的依賴sso-server一致.
  2. 啓動類配置

    @SpringBootApplication
    @RestController
    @EnableOAuth2Sso //應用端SSO登陸須要添加此註解
    public class SsoClientApplication {
    
        @GetMapping("/user")
        public Object user(Authentication user){
            return user;
        }
    
        public static void main(String[] args) {
            SpringApplication.run(SsoClientApplication.class,args);
        }
    }
  3. 配置文件配置
    由於咱們做爲客戶端去訪問認證服務器。

    security:
      oauth2:
        client:
          clientId: clientB
          clientSecret: clientBsecret
          #配置應用A須要認證時候,認證服務器地址,應用跳轉進行認證的url
          user-authorization-uri: http://127.0.0.1:9999/server/oauth/authorize
          #配置應用A須要認證完成以後,認證服務器返回的獲取token的地址
          access-token-uri: http://127.0.0.1:9999/server/oauth/token
        resource:
          jwt:
            # 用戶獲取到jwt後須要使用祕鑰解析jwt時候的祕鑰生成地址  
            key-uri: http://127.0.0.1:9999/server/oauth/token_key
    server:
      port: 7777
      context-path: /clientB
  4. 頁面配置
    頁面配置跳轉到clientB

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>SSO ClientB</title>
    </head>
    <body>
        <h1>SSO Demo ClientB</h1>
        <a href="http://127.0.0.1:8060/clientA/index.html">訪問ClientA</a>
    </body>
    </html>

2.6 測試

咱們訪問服務A:http://127.0.0.1:7777/clientA/index.html
服務A會

image.png

clientA沒有作個性化配置,實際上使用了spring security的默認安全配置。全部url都會受到保護,訪問全部url都須要身份認證。那麼咱們一訪問index.html頁面就會把咱們的請求轉到認證服務器上作認證。去請求受權。

clientA沒有作個性化配置,實際上使用了spring security的默認安全配置。全部url都會受到保護,訪問全部url都須要身份認證。那麼咱們一訪問index.html頁面就會把咱們的請求轉到認證服務器上作認證。去請求受權。

http://127.0.0.1:9999/server/oauth/authorize?client_id=clientA&redirect_uri=http://127.0.0.1:7777/clientA/login&response_type=code&state=OGI2Q7

咱們上面雖然跳轉到到了認證服務器上:http://127.0.0.1:9999/server/oauth/authorize可是認證服務器也是不知道我是誰,他須要咱們用戶作一個登陸。由於咱們以前也是沒作什麼配置,因此他會按照spring security默認安全配置,彈出一個http basic的認證塊,讓我輸入用戶名/密碼。

image.png

登陸以後跳轉到受權頁面:提示登陸用戶,你是否受權clientA來訪問你受保護資源。

咱們點擊容許,就會跳轉到:http://127.0.0.1:7777/clientA/index.html

image.png

這個時候咱們已經作了身份認證了,若是沒有作身份認證咱們是看不到這個頁面的。

而後咱們訪問下(查看用戶):http://127.0.0.1:7777/clientA/user
image.png

咱們能查看到客戶信息。
咱們點擊"訪問ClientB",這個時候至關於咱們用戶直接請求ClientB對應的請求,此時ClientB也是不識別我這個用戶,也會直接跳轉到認證服務器上的。

image.png

http://127.0.0.1:9999/server/oauth/authorize?client_id=clientB&redirect_uri=http://127.0.0.1:8888/clientB/login&response_type=code&state=vL2MJw

image.png

這個時候咱們點擊受權:咱們會直接跳轉到:http://127.0.0.1:8888/clientB/index.html

咱們發現此時咱們沒有進行登陸:由於此時認證服務器是知道個人身份的,咱們已經在應用A訪問時候已經登陸認證過了。此時提示你是否受權:clientB來訪問。

最後咱們就能夠在clientA和ClientB的index.html上隨便切換訪問了。
查看Jwt信息:
訪問應用A:http://127.0.0.1:7777/clientA/user

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODM4NjkwNjQsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiM2RlYTIzNDEtZjA3Yi00MDBkLWFmNTctMGUyOWI2MWZmZWJiIiwiY2xpZW50X2lkIjoiY2xpZW50QSIsInNjb3BlIjpbImFsbCJdfQ.wPNWgRJR2yI9mq4t3ZZS81H3TpErmwkekQp3hiYEUjI

對應用戶信息:

訪問應用B:http://127.0.0.1:8888/clientB/user

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODM4Njk1NzksInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiY2YzY2E5ZTctYzkyYS00M2Y4LWJjY2YtYWUwNjY4YjNlZWYxIiwiY2xpZW50X2lkIjoiY2xpZW50QiIsInNjb3BlIjpbImFsbCJdfQ.nRmh-BtqpuTucG2s4iVDdqStCeKApvioA9W953F3XUU

咱們經過最後幾位發現其對應的JWT是不同的。可是對應的用戶信息實際上是同樣的。當時他們能從不一樣的jwt中解析出相同用戶信息作登陸。

上面最核心的功能實現了:只登錄一次,而後用這一次登錄信息訪問應用A和應用B

3 單點登陸優化

3.1 存在的問題

  1. 咱們在認證服務器上登陸的時候默認是http basic彈出快輸入用戶名/密碼。咱們但願展現給用戶的美觀頁面去登陸
  2. 咱們第一次訪問應用A或者應用B的時候,都會給咱們一個受權,而後點擊按鈕贊成受權才讓訪問,其實咱們受權是在咱們後端服務作的,不用咱們前端去吊機控制。

3.2 代碼改造

  1. 咱們如何把http的basic登陸變成表單登陸。
    咱們新增:SsoSecurityConfig;修改裏面的configure方法。

    @Configuration
    public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
             //http表單登陸的全部請求都須要受權
             http.formLogin().and().authorizeRequests().anyRequest().authenticated();
        }
    }
  2. 咱們不使用application.yml文件裏面配置的信息

    security:
      user:
        password: 123456

而是使用咱們數據庫裏面的用戶名/密碼,咱們此時須要自定義;其中密碼的話,咱們設置爲Spring Security推薦的加密編碼格式。並覆蓋掉咱們:AuthenticationManager的配置,告訴他用我本身的UserDetailsService。
和加密器用做身份認證。

@Configuration
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService ssoUserDetailsService;

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
         //http表單登陸的全部請求都須要受權
         http.formLogin().and().authorizeRequests().anyRequest().authenticated();
    }
}

UserDetailsService

@Component
public class SsoUserDetailsService  implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // public User(String username, String password, Collection<? extends GrantedAuthority> authorities)
        return new User(username,passwordEncoder.encode("123456"),
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

重啓服務在此訪問:
http://127.0.0.1:8888/clientB/index.html

image.png

此時用戶訪問應用A時候,會跳到認證服務器進行認證。

而後跳轉到:

http://127.0.0.1:9999/server/oauth/authorize?client_id=clientB&redirect_uri=http://127.0.0.1:8888/clientB/login&response_type=code&state=8WbHSO

image.png

點擊受權,而後跳轉到咱們受權的頁面:
image.png

再點擊"訪問ClientA",也會跳轉到認證服務器的受權頁面
image.png

而後點擊受權進入:ClientA頁面。
image.png

最後能夠在ClientA和clientB之間輪流切換。

咱們如今須要登陸以後不受權,直接跳轉到對應的頁面,受權哪一個頁面咱們沒辦法跳過去的,由於受權碼oauth2協議決定了。

咱們的思路是:找到受權對應表單,而後找到具體是從哪裏生成出來的,而後改造生成表單的頁面,一進頁面就自動的提交掉。用戶不須要直接去點擊。

咱們跟蹤代碼了,發現是:WhitelabelApprovalEndpoint

@FrameworkEndpoint
@SessionAttributes({"authorizationRequest"})
public class WhitelabelApprovalEndpoint {
    private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
    private static String DENIAL = "<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input name='deny' value='Deny' type='submit'/></label></form>";
    private static String TEMPLATE = "<html><body><h1>OAuth Approval</h1><p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p><form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>%denial%</body></html>";
    private static String SCOPE = "<li><div class='form-group'>%scope%: <input type='radio' name='%key%' value='true'%approved%>Approve</input> <input type='radio' name='%key%' value='false'%denied%>Deny</input></div></li>";

    public WhitelabelApprovalEndpoint() {
    }

    @RequestMapping({"/oauth/confirm_access"})
    public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
        String template = this.createTemplate(model, request);
        if (request.getAttribute("_csrf") != null) {
            model.put("_csrf", request.getAttribute("_csrf"));
        }

        return new ModelAndView(new SpelView(template), model);
    }

    protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
        String template = TEMPLATE;
        if (!model.containsKey("scopes") && request.getAttribute("scopes") == null) {
            template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
        } else {
            template = template.replace("%scopes%", this.createScopes(model, request)).replace("%denial%", "");
        }

        if (!model.containsKey("_csrf") && request.getAttribute("_csrf") == null) {
            template = template.replace("%csrf%", "");
        } else {
            template = template.replace("%csrf%", CSRF);
        }

        return template;
    }

    private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
        StringBuilder builder = new StringBuilder("<ul>");
        Map<String, String> scopes = (Map)((Map)(model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes")));
        Iterator var5 = scopes.keySet().iterator();

        while(var5.hasNext()) {
            String scope = (String)var5.next();
            String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
            String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
            String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved).replace("%denied%", denied);
            builder.append(value);
        }

        builder.append("</ul>");
        return builder.toString();
    }
}

其@FrameworkEndpoint註解與@RestController相似。其下可使用註解: @RequestMapping({"/oauth/confirm_access"})訪問。

咱們新建一個與上面同名字的類:WhitelabelApprovalEndpoint使用@RestController來註解,spring在處理的時候優先會處理執行@RestController標註的類。類名和@RestController與WhitelabelApprovalEndpoint不同,其餘照搬。

@RestController
@SessionAttributes({"authorizationRequest"})
public class SsoApprovalEndpoint {
    private static String CSRF = "<input type='hidden' name='${_csrf.parameterName}' value='${_csrf.token}' />";
    private static String DENIAL = "<form id='denialForm' name='denialForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='false' type='hidden'/>%csrf%<label><input name='deny' value='Deny' type='submit'/></label></form>";
    private static String TEMPLATE = "<html><body><h1>OAuth Approval</h1><p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p><form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>%denial%</body></html>";
    private static String SCOPE = "<li><div class='form-group'>%scope%: <input type='radio' name='%key%' value='true'%approved%>Approve</input> <input type='radio' name='%key%' value='false'%denied%>Deny</input></div></li>";

    public SsoApprovalEndpoint() {
    }

    @RequestMapping({"/oauth/confirm_access"})
    public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
        String template = this.createTemplate(model, request);
        if (request.getAttribute("_csrf") != null) {
            model.put("_csrf", request.getAttribute("_csrf"));
        }

        return new ModelAndView(new SsoSpelView(template), model);
    }

    protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
        String template = TEMPLATE;
        if (!model.containsKey("scopes") && request.getAttribute("scopes") == null) {
            template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
        } else {
            template = template.replace("%scopes%", this.createScopes(model, request)).replace("%denial%", "");
        }

        if (!model.containsKey("_csrf") && request.getAttribute("_csrf") == null) {
            template = template.replace("%csrf%", "");
        } else {
            template = template.replace("%csrf%", CSRF);
        }

        return template;
    }

    private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) {
        StringBuilder builder = new StringBuilder("<ul>");
        Map<String, String> scopes = (Map)((Map)(model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes")));
        Iterator var5 = scopes.keySet().iterator();

        while(var5.hasNext()) {
            String scope = (String)var5.next();
            String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
            String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
            String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved).replace("%denied%", denied);
            builder.append(value);
        }

        builder.append("</ul>");
        return builder.toString();
    }
}

因爲裏面會使用一個實現View接口的類,因此咱們

image.png

裏面的SpelView類不是共有的:public 因此咱們不能複用,須要自定義一個實現View接口歐的類。

public class SsoSpelView implements View {
    private final String template;
    private final String prefix;
    private final SpelExpressionParser parser = new SpelExpressionParser();
    private final StandardEvaluationContext context = new StandardEvaluationContext();
    private PropertyPlaceholderHelper.PlaceholderResolver resolver;

    public SsoSpelView(String template) {
        this.template = template;
        this.prefix = (new RandomValueStringGenerator()).generate() + "{";
        this.context.addPropertyAccessor(new MapAccessor());
        this.resolver = new PropertyPlaceholderHelper.PlaceholderResolver() {
            public String resolvePlaceholder(String name) {
                Expression expression = SsoSpelView.this.parser.parseExpression(name);
                Object value = expression.getValue(SsoSpelView.this.context);
                return value == null ? null : value.toString();
            }
        };
    }

    public String getContentType() {
        return "text/html";
    }

    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        Map<String, Object> map = new HashMap(model);
        String path = ServletUriComponentsBuilder.fromContextPath(request).build().getPath();
        map.put("path", path == null ? "" : path);
        this.context.setRootObject(map);
        String maskedTemplate = this.template.replace("${", this.prefix);
        PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(this.prefix, "}");
        String result = helper.replacePlaceholders(maskedTemplate, this.resolver);
        result = result.replace(this.prefix, "${");
        response.setContentType(this.getContentType());
        response.getWriter().append(result);
    }
}

咱們看到前端生成的頁面就是屬性TEMPLATE:

private static String TEMPLATE = "<html><body><h1>OAuth Approval</h1><p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?</p><form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'><input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>%denial%</body></html>";

咱們修改這個屬性:添加div設置裏面的display:none 而後作一個表單的提交。

private static String TEMPLATE = "<html><body><div style='display:none'><h1>OAuth Approval</h1><p>Do you authorize '${authorizationRequest.clientId}' to access your protected resources?" +
            "</p><form id='confirmationForm' name='confirmationForm' action='${path}/oauth/authorize' method='post'>" +
            "<input name='user_oauth_approval' value='true' type='hidden'/>%csrf%%scopes%<label><input name='authorize' value='Authorize' type='submit'/></label></form>%denial%" +
            "</div><script>document.getElementById('confirmationForm').submit()</script></body></html>";

重啓服務,而後咱們訪問clientA的首頁

image.png

咱們點擊用戶名/密碼登陸,而後頁面一閃就到達頁面:

image.png

而後咱們點擊"訪問ClientB",一閃跳到ClientA
image.png

相關文章
相關標籤/搜索