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

相关文章
相关标签/搜索