前言
什么是单点登录?
我们看一个例子:我们访问taobao的时候,在点击下淘宝主页的天猫,我们发现其实他是两个域名;所以应该是不同的服务器。
然后我们再次登录下淘宝账号:
淘宝登录的域名又是和淘宝/天猫首页的不一致,三个不同的域名代表了3个不同服务器。
然后我们输入账号登录淘宝,然后刷新下天猫,发现天猫也登陆了。
单点登录:我们登录一台服务器(系统)之后,同时也登录了另一台服务(系统)。
内容
1. JWT实现SSO的流程
基于JWT实现SSO登录。
- 我们有三台服务器:应用A、应用B、认证服务器;类比于我们的淘宝(应用A)、天猫(应用B)、淘宝登录页(认证服务器)。
- 当一个访问请求发给我们应用A时候,如果这个请求只有在登录以后才能访问,那么应用A就会向认证服务器请求授权(把用户引导到认证服务器上)。那么此时用户在认证服务器上进行认证并授权。注意用户在做登录动作是在认证服务器上做的。用户一旦认证/授权之后就会返回授权码给应用A,那么应用A拿着这个授权码去请求令牌,认证服务器给应用A返回JWT。应用A解析这里面信息,然后并登录。此时用户就在应用A上登录。 在第二步流程走完之后,我们返回的JWT其实是包含当前用户在认证服务器上进行登录以后所登录的用户信息都是放在JWT信息里面给返回回去的。那么这个应用A他解析里面JWT信息,然后用这些信息生成自个认证的Authentication放到他的Spring Security的Security Context里面,这样的话它相当于利用这个用户的这些信息在他自己的网站上进行了登录,那么后续的请求可以直接访问应用A上的资源了。
- 当他在应用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是很容易实现的。
2.2 搭建工程
我们怎么不在原来的代码上去写了,有2个原因: 原因一:原来代码结构并不适合我当前sso登录场景。原来代码结构是按照浏览器安全session的安全 我们需要怎样控制?基于App的安全令牌token的方式我们如何控制?,是按照上面2种方式来区分开的。 但是在sso的模式下,认证服务器是一个特殊存在,他是由基于浏览器的处理,各种跳转,session的处理,另外一部分,他也会发令牌。基于浏览器,基于session,同时也要发令牌。他会混合我们之前讲解的所有东西。 原因二:在原来基础上,我们修改代码是可以实现sso的,但是代码的复杂度会加大。 我们单独使用工程搭建,实现功能不会操作100行代码,可以把之前所有的代码逻辑实现全部串起来。
其中
oss-server:认证服务器
oss-clientA:应用服务A
oss-clientB:应用服务B
2.3 sso-server工程
- 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>
然后创建启动类
- 配置标准认证服务器
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;
}
}
- 添加配置文件
用户认证的认证动作是在认证服务器上运行的,所以我们需要在认证服务器上配置用户信息。
添加application.yml文件,里面的内容是:
server:
port: 9999
context-path: /server
security:
user:
password: 123456
后面我们从应用A跳转到认证服务器时候输入的密码是要输入:123456
2.4 sso-clientA工程
- 添加依赖:添加的依赖sso-server一致.
-
启动类配置
@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); } }
-
配置文件配置
因为我们作为客户端去访问认证服务器。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
-
页面配置
页面配置跳转到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工程
- 添加依赖:添加的依赖sso-server一致.
-
启动类配置
@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); } }
-
配置文件配置
因为我们作为客户端去访问认证服务器。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
-
页面配置
页面配置跳转到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会
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的认证块,让我输入用户名/密码。
登录之后跳转到授权页面:提示登录用户,你是否授权clientA来访问你受保护资源。
我们点击允许,就会跳转到:http://127.0.0.1:7777/clientA/index.html
这个时候我们已经做了身份认证了,如果没有做身份认证我们是看不到这个页面的。
然后我们访问下(查看用户):http://127.0.0.1:7777/clientA/user
我们能查看到客户信息。
我们点击"访问ClientB",这个时候相当于我们用户直接请求ClientB对应的请求,此时ClientB也是不识别我这个用户,也会直接跳转到认证服务器上的。
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
这个时候我们点击授权:我们会直接跳转到: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 存在的问题
- 我们在认证服务器上登录的时候默认是http basic弹出快输入用户名/密码。我们希望展示给用户的美观页面去登录
- 我们第一次访问应用A或者应用B的时候,都会给我们一个授权,然后点击按钮同意授权才让访问,其实我们授权是在我们后端服务做的,不用我们前端去吊机控制。
3.2 代码改造
-
我们如何把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(); } }
-
我们不使用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
此时用户访问应用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
点击授权,然后跳转到我们授权的页面:
再点击"访问ClientA",也会跳转到认证服务器的授权页面
然后点击授权进入:ClientA页面。
最后可以在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接口的类,所以我们
里面的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的首页
我们点击用户名/密码登录,然后页面一闪就到达页面:
然后我们点击"访问ClientB",一闪跳到ClientA
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。