提交 6d3f8a75 编写于 作者: zlt2000's avatar zlt2000

增加oidc协议单点登录样例工程oidc-sso

上级 6d50fc99
* **ss-sso**:使用springSecurity来实现自动单点登录,非前后端分离
* **web-sso**:前后端分离的单点登录
* **oidc-sso**:拥有独立用户体系的系统,使用OIDC协议的单点登录
## **详细的原理和注意事项请查看**
[OIDC协议单点登录](https://www.kancloud.cn/zlt2000/microservices-platform/2278851)
## oauth-center数据库执行以下sql
```sql
alter table oauth_client_details add support_id_token tinyint(1) DEFAULT 1 COMMENT '是否支持id_token';
alter table oauth_client_details add id_token_validity int(11) DEFAULT 60 COMMENT 'id_token有效期';
```
## 启动以下服务
1. zlt-uaa:统一认证中心
2. user-center:用户服务
3. sc-gateway:api网关
4. oidc-sso:单点登录demo(app应用)
5. ss-sso:单点登录demo(zlt应用)
## 测试步骤
1. 登录zlt应用:
通过地址 http://127.0.0.1:8080 先登录zlt应用
2. 访问app应用(单点成功):
在浏览器打开一个新的页签(共享session),通过地址 http://127.0.0.1:8081/index.html 访问zlt应用静态页面,单点登录成功显示当前登录用户名、应用id、token信息
\ No newline at end of file
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zlt</groupId>
<artifactId>sso-demo</artifactId>
<version>4.4</version>
</parent>
<artifactId>oidc-sso</artifactId>
<description>OIDC协议单点登录demo</description>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>com.zlt</groupId>
<artifactId>zlt-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
package com.sso.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author zlt
* @date 2020/5/22
* <p>
* Blog: https://zlt2000.gitee.io
* Github: https://github.com/zlt2000
*/
@SpringBootApplication
public class OidcSSOApplication {
public static void main(String[] args) {
SpringApplication.run(OidcSSOApplication.class, args);
}
}
package com.sso.demo.controller;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.central.common.model.Result;
import com.central.common.utils.JsonUtil;
import com.central.common.utils.RsaUtils;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import sun.misc.BASE64Encoder;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPublicKey;
import java.util.*;
/**
* @author zlt
* @date 2020/5/22
* <p>
* Blog: https://zlt2000.gitee.io
* Github: https://github.com/zlt2000
*/
@RestController
public class ApiController {
private static final String PUBKEY_START = "-----BEGIN PUBLIC KEY-----";
private static final String PUBKEY_END = "-----END PUBLIC KEY-----";
@Value("${zlt.sso.client-id:}")
private String clientId;
@Value("${zlt.sso.client-secret:}")
private String clientSecret;
@Value("${zlt.sso.redirect-uri:}")
private String redirectUri;
@Value("${zlt.sso.access-token-uri:}")
private String accessTokenUri;
@Value("${zlt.sso.jwt-key-uri:}")
private String jwtKeyUri;
/**
* 公钥
*/
private static RSAPublicKey publicKey;
/**
* 模拟用户数据库
*/
private static final Map<Long, MyUser> userDb = new HashMap<>();
/**
* nonce存储
*/
private final static ThreadLocal<String> NONCE = new ThreadLocal<>();
@GetMapping("/token/{code}")
public Map<String, Object> tokenInfo(@PathVariable String code) throws Exception {
//获取token
Map<String, Object> tokenMap = getAccessToken(code);
String idTokenStr = (String)tokenMap.get("id_token");
//解析id_token
JsonNode idToken = this.getIdTokenJson(idTokenStr);
//检查id_token的有效性
checkToken(idToken);
//获取用户信息
MyUser user = this.getUserInfo(idToken);
//判断用户信息是否存在,否则注册用户信息
if (!userDb.containsKey(user.getId())) {
userDb.put(user.getId(), user);
}
Map<String, Object> result = new HashMap<>(2);
result.put("tokenInfo", tokenMap);
result.put("userInfo", user);
return result;
}
/**
* 检查 id_token 的有效性
*/
private void checkToken(JsonNode idToken) {
//token有效期
long expiresAt = idToken.get("exp").asLong();
long now = System.currentTimeMillis();
Assert.isTrue((expiresAt > now), "id_token已过期");
//应用id
String aud = idToken.get("aud").asText();
Assert.isTrue(clientId.equals(aud), "非法应用"+aud);
//随机码
String nonce = idToken.get("nonce").asText();
Assert.isTrue((StrUtil.isEmpty(nonce) || nonce.equals(NONCE.get())), "nonce参数无效");
}
/**
* 获取token
*/
public Map<String, Object> getAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
String base64Auth = this.getBase64ClientParam();
headers.add("Authorization", "Basic " + base64Auth);
MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
param.add("code", code);
param.add("grant_type", "authorization_code");
param.add("redirect_uri", redirectUri);
param.add("scope", "app");
param.add("nonce", this.genNonce());
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(param, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(accessTokenUri, request , Map.class);
return response.getBody();
}
private String genNonce() {
String nonce = RandomUtil.randomString(6);
NONCE.set(nonce);
return nonce;
}
/**
* 把 id_token 字符串解析为json对象
*/
public JsonNode getIdTokenJson(String idToken) throws Exception {
RSAPublicKey publicKey = getPubKeyObj();
return this.decodeAndVerify(idToken, publicKey);
}
/**
* 通过 id_token 获取用户信息
*/
public MyUser getUserInfo(JsonNode idToken) {
MyUser user = new MyUser();
user.setId(Long.valueOf(idToken.get("sub").textValue()));
user.setName(idToken.get("name").textValue());
user.setLoginName(idToken.get("login_name").textValue());
user.setPicture(idToken.get("picture").textValue());
return user;
}
private JsonNode decodeAndVerify(String jwtToken, RSAPublicKey rsaPublicKey) {
SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey);
Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier);
return JsonUtil.parse(jwt.getClaims());
}
/**
* 获取公钥
*/
public RSAPublicKey getPubKeyObj() throws Exception {
if (publicKey == null) {
publicKey = getPubKeyByRemote();
}
return publicKey;
}
private RSAPublicKey getPubKeyByRemote() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
String base64Auth = this.getBase64ClientParam();
headers.add("Authorization", "Basic " + base64Auth);
HttpEntity<String> request = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Result> response = restTemplate.exchange(jwtKeyUri, HttpMethod.GET, request, Result.class);
Result<String> result = response.getBody();
Assert.isTrue((result.getResp_code() == 200), result.getResp_msg());
String publicKeyStr = result.getResp_msg();
publicKeyStr = publicKeyStr.substring(PUBKEY_START.length(), publicKeyStr.indexOf(PUBKEY_END));
return RsaUtils.getPublicKey(publicKeyStr);
}
/**
* base64加密应用参数
*/
private String getBase64ClientParam() {
byte[] authorization = (clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8);
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(authorization);
}
@Data
public static class MyUser {
private Long id;
private String name;
private String loginName;
private String picture;
}
}
server:
port: 8081
spring:
application:
name: oidc-demo
zlt:
sso:
client-id: app
client-secret: app
redirect-uri: http://127.0.0.1:8081/callback.html
access-token-uri: http://127.0.0.1:9900/api-uaa/oauth/token
jwt-key-uri: http://127.0.0.1:9900/api-uaa/tokens/key
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta charset="utf-8"/>
<title>zlt</title>
<script type="text/javascript" src="js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="js/sso.js"></script>
</head>
<body>
<script>
window.onload = function() {
//url获取state
let state = getQueryVariable('state');
let localState = sessionStorage.getItem("state");
//判断state防止CSRF攻击
if (localState !== state) {
alert('state参数无效!');
let state = getState();
sessionStorage.setItem("state", state);
window.location = getAuthorizeUri(state);
}
//url获取code
let code = getQueryVariable('code');
//获取token和用户信息
$.ajax({url:'http://127.0.0.1:8081/token/'+code, success:function(result) {
console.log(result);
sessionStorage.setItem('access_token', result.tokenInfo.access_token);
sessionStorage.setItem('username', result.userInfo.name);
sessionStorage.setItem('loginName', result.userInfo.loginName);
sessionStorage.setItem('picture', result.userInfo.picture);
window.location = sessionStorage.getItem('visitUri');
}});
};
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta charset="utf-8"/>
<title>zlt</title>
<script type="text/javascript" src="js/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="js/sso.js"></script>
</head>
<body>
<div>
<p>用户名:<span id="userName"></span></p>
<p>登录名:<span id="loginName"></span></p>
<p>头像:<span id="picture"></span></p>
<p>应用id:<span id="clientId"></span></p>
<p>token:<span id="accessToken"></span></p>
<p><input type="button" value="登出" onclick="logout()"/></p>
</div>
<script>
window.onload = function() {
let accessToken = sessionStorage.getItem('access_token');
if (accessToken) {//已登录
let username = sessionStorage.getItem('username');
let loginName = sessionStorage.getItem("loginName");
let picture = sessionStorage.getItem("picture");
$('#accessToken').html(accessToken);
$('#userName').html(username);
$('#loginName').html(loginName);
$('#picture').html(picture);
$('#clientId').html(clientId);
} else {//未登录
let state = getState();
sessionStorage.setItem("visitUri", window.location.href);
sessionStorage.setItem("state", state);
window.location = getAuthorizeUri(state);
}
};
function logout() {
let accessToken = sessionStorage.getItem('access_token');
sessionStorage.removeItem('access_token');
sessionStorage.removeItem('username');
sessionStorage.removeItem("loginName");
sessionStorage.removeItem("picture");
window.location = getLogoutUri(accessToken);
}
</script>
</body>
</html>
const FULL_CHARTER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopgrstuvwxyz';
//应用id
let clientId = 'app';
//授权中心地址
let uaaUri = 'http://127.0.0.1:9900/api-uaa/oauth/';
function getAuthorizeUri(state) {
return uaaUri+'authorize?client_id='+clientId+'&redirect_uri=http://127.0.0.1:8081/callback.html&response_type=code%20id_token&state='+state;
}
function getLogoutUri(accessToken) {
return uaaUri+'remove/token?redirect_uri=http://127.0.0.1:8081/index.html&access_token='+accessToken;
}
function getState() {
let state='';
for (let i = 0; i < 6; i++) {
state += FULL_CHARTER[Math.floor(Math.random() * 52)];
}
return state;
}
/**
* 获取url参数
*/
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return '';
}
\ No newline at end of file
......@@ -14,5 +14,7 @@
<module>ss-sso</module>
<!-- 前后端分离单点登录demo -->
<module>web-sso</module>
<!-- OIDC协议单点登录demo -->
<module>oidc-sso</module>
</modules>
</project>
\ No newline at end of file
## **详细的原理和注意事项请查看**
[单点登录详解](https://www.kancloud.cn/zlt2000/microservices-platform/1515193)
[单点登录详解](https://www.kancloud.cn/zlt2000/microservices-platform/2278849)
......
## **详细的原理和注意事项请查看**
[单点登录详解](https://www.kancloud.cn/zlt2000/microservices-platform/1515193)
[前后端分离的单点登录](https://www.kancloud.cn/zlt2000/microservices-platform/2278850)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册