Commit 0df44b51e43c5d1501d8e64b3a28a77d26243a1d
1 parent
eef23364
完成 yudao-sso-demo-by-code 使用 code 授权码,获得访问令牌的逻辑
Showing
8 changed files
with
243 additions
and
8 deletions
yudao-example/yudao-sso-demo-by-code/pom.xml
| @@ -48,6 +48,12 @@ | @@ -48,6 +48,12 @@ | ||
| 48 | <groupId>org.springframework.boot</groupId> | 48 | <groupId>org.springframework.boot</groupId> |
| 49 | <artifactId>spring-boot-starter-security</artifactId> | 49 | <artifactId>spring-boot-starter-security</artifactId> |
| 50 | </dependency> | 50 | </dependency> |
| 51 | + | ||
| 52 | + <dependency> | ||
| 53 | + <groupId>org.projectlombok</groupId> | ||
| 54 | + <artifactId>lombok</artifactId> | ||
| 55 | + <optional>true</optional> | ||
| 56 | + </dependency> | ||
| 51 | </dependencies> | 57 | </dependencies> |
| 52 | 58 | ||
| 53 | </project> | 59 | </project> |
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java
0 → 100644
| 1 | +package cn.iocoder.yudao.ssodemo.client; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||
| 4 | +import cn.iocoder.yudao.ssodemo.client.dto.OAuth2AccessTokenRespDTO; | ||
| 5 | +import org.springframework.core.ParameterizedTypeReference; | ||
| 6 | +import org.springframework.http.*; | ||
| 7 | +import org.springframework.stereotype.Component; | ||
| 8 | +import org.springframework.util.Assert; | ||
| 9 | +import org.springframework.util.Base64Utils; | ||
| 10 | +import org.springframework.util.LinkedMultiValueMap; | ||
| 11 | +import org.springframework.util.MultiValueMap; | ||
| 12 | +import org.springframework.web.client.RestTemplate; | ||
| 13 | + | ||
| 14 | +import java.nio.charset.StandardCharsets; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * OAuth 2.0 客户端 | ||
| 18 | + */ | ||
| 19 | +@Component | ||
| 20 | +public class OAuth2Client { | ||
| 21 | + | ||
| 22 | + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2/"; | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 租户编号 | ||
| 26 | + * | ||
| 27 | + * 默认使用 1;如果使用别的租户,可以调整 | ||
| 28 | + */ | ||
| 29 | + private static final Long TENANT_ID = 1L; | ||
| 30 | + | ||
| 31 | + private static final String CLIENT_ID = "yudao-sso-demo-by-code"; | ||
| 32 | + private static final String CLIENT_SECRET = "test"; | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 | ||
| 36 | + private final RestTemplate restTemplate = new RestTemplate(); | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 使用 code 授权码,获得访问令牌 | ||
| 40 | + * | ||
| 41 | + * @param code 授权码 | ||
| 42 | + * @param redirectUri 重定向 URI | ||
| 43 | + * @return 访问令牌 | ||
| 44 | + */ | ||
| 45 | + public CommonResult<OAuth2AccessTokenRespDTO> postAccessToken(String code, String redirectUri) { | ||
| 46 | + // 1.1 构建请求头 | ||
| 47 | + HttpHeaders headers = new HttpHeaders(); | ||
| 48 | + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||
| 49 | + headers.set("tenant-id", TENANT_ID.toString()); | ||
| 50 | + addClientHeader(headers); | ||
| 51 | + // 1.2 构建请求参数 | ||
| 52 | + MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); | ||
| 53 | + body.add("grant_type", "authorization_code"); | ||
| 54 | + body.add("code", code); | ||
| 55 | + body.add("redirect_uri", redirectUri); | ||
| 56 | +// body.add("state", ""); // 选填;填了会校验 | ||
| 57 | + | ||
| 58 | + // 2. 执行请求 | ||
| 59 | + ResponseEntity<CommonResult<OAuth2AccessTokenRespDTO>> exchange = restTemplate.exchange( | ||
| 60 | + BASE_URL + "/token", | ||
| 61 | + HttpMethod.POST, | ||
| 62 | + new HttpEntity<>(body, headers), | ||
| 63 | + new ParameterizedTypeReference<CommonResult<OAuth2AccessTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失 | ||
| 64 | + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); | ||
| 65 | + return exchange.getBody(); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + private static void addClientHeader(HttpHeaders headers) { | ||
| 69 | + // client 拼接,需要 BASE64 编码 | ||
| 70 | + String client = CLIENT_ID + ":" + CLIENT_SECRET; | ||
| 71 | + client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); | ||
| 72 | + headers.add("Authorization", "Basic " + client); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | +} |
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/CommonResult.java
0 → 100644
| 1 | +package cn.iocoder.yudao.ssodemo.client.dto; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.io.Serializable; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 通用返回 | ||
| 9 | + * | ||
| 10 | + * @param <T> 数据泛型 | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +public class CommonResult<T> implements Serializable { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 错误码 | ||
| 17 | + */ | ||
| 18 | + private Integer code; | ||
| 19 | + /** | ||
| 20 | + * 返回数据 | ||
| 21 | + */ | ||
| 22 | + private T data; | ||
| 23 | + /** | ||
| 24 | + * 错误提示,用户可阅读 | ||
| 25 | + */ | ||
| 26 | + private String msg; | ||
| 27 | + | ||
| 28 | +} |
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/OAuth2AccessTokenRespDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.ssodemo.client.dto; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.annotation.JsonProperty; | ||
| 4 | +import lombok.AllArgsConstructor; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import lombok.NoArgsConstructor; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 访问令牌 Response DTO | ||
| 10 | + */ | ||
| 11 | +@Data | ||
| 12 | +@NoArgsConstructor | ||
| 13 | +@AllArgsConstructor | ||
| 14 | +public class OAuth2AccessTokenRespDTO { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 访问令牌 | ||
| 18 | + */ | ||
| 19 | + @JsonProperty("access_token") | ||
| 20 | + private String accessToken; | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 刷新令牌 | ||
| 24 | + */ | ||
| 25 | + @JsonProperty("refresh_token") | ||
| 26 | + private String refreshToken; | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 令牌类型 | ||
| 30 | + */ | ||
| 31 | + @JsonProperty("token_type") | ||
| 32 | + private String tokenType; | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 过期时间;单位:秒 | ||
| 36 | + */ | ||
| 37 | + @JsonProperty("expires_in") | ||
| 38 | + private Long expiresIn; | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 授权范围;如果多个授权范围,使用空格分隔 | ||
| 42 | + */ | ||
| 43 | + private String scope; | ||
| 44 | + | ||
| 45 | +} |
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java
| 1 | package cn.iocoder.yudao.ssodemo.controller; | 1 | package cn.iocoder.yudao.ssodemo.controller; |
| 2 | 2 | ||
| 3 | -import org.springframework.stereotype.Controller; | 3 | +import cn.iocoder.yudao.ssodemo.client.OAuth2Client; |
| 4 | +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; | ||
| 5 | +import cn.iocoder.yudao.ssodemo.client.dto.OAuth2AccessTokenRespDTO; | ||
| 4 | import org.springframework.web.bind.annotation.PostMapping; | 6 | import org.springframework.web.bind.annotation.PostMapping; |
| 7 | +import org.springframework.web.bind.annotation.RequestMapping; | ||
| 8 | +import org.springframework.web.bind.annotation.RequestParam; | ||
| 9 | +import org.springframework.web.bind.annotation.RestController; | ||
| 5 | 10 | ||
| 6 | -@Controller("/auth") | 11 | +import javax.annotation.Resource; |
| 12 | + | ||
| 13 | +@RestController | ||
| 14 | +@RequestMapping("/auth") | ||
| 7 | public class AuthController { | 15 | public class AuthController { |
| 8 | 16 | ||
| 9 | - @PostMapping("/login-by-code") | ||
| 10 | - public void loginByCode() { | 17 | + @Resource |
| 18 | + private OAuth2Client oauth2Client; | ||
| 11 | 19 | ||
| 20 | + /** | ||
| 21 | + * 使用 code 访问令牌,获得访问令牌 | ||
| 22 | + * | ||
| 23 | + * @param code 授权码 | ||
| 24 | + * @param redirectUri 重定向 URI | ||
| 25 | + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 | ||
| 26 | + */ | ||
| 27 | + @PostMapping("/login-by-code") | ||
| 28 | + public CommonResult<OAuth2AccessTokenRespDTO> loginByCode(@RequestParam("code") String code, | ||
| 29 | + @RequestParam("redirectUri") String redirectUri) { | ||
| 30 | + return oauth2Client.postAccessToken(code, redirectUri); | ||
| 12 | } | 31 | } |
| 13 | 32 | ||
| 14 | } | 33 | } |
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/SecurityConfiguration.java
| @@ -10,9 +10,12 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { | @@ -10,9 +10,12 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { | ||
| 10 | 10 | ||
| 11 | @Override | 11 | @Override |
| 12 | protected void configure(HttpSecurity httpSecurity) throws Exception { | 12 | protected void configure(HttpSecurity httpSecurity) throws Exception { |
| 13 | - httpSecurity.authorizeRequests() | 13 | + httpSecurity.csrf().disable() // 禁用 CSRF 保护 |
| 14 | + .authorizeRequests() | ||
| 14 | // 1. 静态资源,可匿名访问 | 15 | // 1. 静态资源,可匿名访问 |
| 15 | .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() | 16 | .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() |
| 17 | + // 2. 登录相关的接口,可匿名访问 | ||
| 18 | + .antMatchers("/auth/login-by-code").permitAll() | ||
| 16 | // last. 兜底规则,必须认证 | 19 | // last. 兜底规则,必须认证 |
| 17 | .and().authorizeRequests() | 20 | .and().authorizeRequests() |
| 18 | .anyRequest().authenticated(); | 21 | .anyRequest().authenticated(); |
yudao-example/yudao-sso-demo-by-code/src/main/resources/static/callback.html
0 → 100644
| 1 | +<!DOCTYPE html> | ||
| 2 | +<html lang="en"> | ||
| 3 | +<head> | ||
| 4 | + <meta charset="UTF-8"> | ||
| 5 | + <title>SSO 授权后的回调页</title> | ||
| 6 | + <!-- jQuery:操作 dom、发起请求等 --> | ||
| 7 | + <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> | ||
| 8 | + <!-- 工具类 --> | ||
| 9 | + <script type="application/javascript"> | ||
| 10 | + (function ($) { | ||
| 11 | + /** | ||
| 12 | + * 获得 URL 的指定参数的值 | ||
| 13 | + * | ||
| 14 | + * @param name 参数名 | ||
| 15 | + * @returns 参数值 | ||
| 16 | + */ | ||
| 17 | + $.getUrlParam = function (name) { | ||
| 18 | + const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); | ||
| 19 | + const r = window.location.search.substr(1).match(reg); | ||
| 20 | + if (r != null) return unescape(r[2]); return null; | ||
| 21 | + } | ||
| 22 | + })(jQuery); | ||
| 23 | + </script> | ||
| 24 | + | ||
| 25 | + <script type="application/javascript"> | ||
| 26 | + $(function () { | ||
| 27 | + // 获得 code 授权码 | ||
| 28 | + const code = $.getUrlParam('code'); | ||
| 29 | + if (!code) { | ||
| 30 | + alert('获取不到 code 参数,请排查!') | ||
| 31 | + return; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + // 提交 | ||
| 35 | + const redirectUri = 'http://127.0.0.1:18080/callback.html'; // 需要修改成,你回调的地址,就是在 index.html 拼接的 redirectUri | ||
| 36 | + $.ajax({ | ||
| 37 | + url: "http://127.0.0.1:18080/auth/login-by-code?code=" + code | ||
| 38 | + + '&redirectUri=' + redirectUri, | ||
| 39 | + method: 'POST', | ||
| 40 | + success: function( result ) { | ||
| 41 | + if (result.code !== 0) { | ||
| 42 | + alert('获得访问令牌失败,原因:' + result.msg) | ||
| 43 | + return; | ||
| 44 | + } | ||
| 45 | + alert('获得访问令牌成功!点击确认,跳转回首页') | ||
| 46 | + | ||
| 47 | + // 设置到 localStorage 中 | ||
| 48 | + localStorage.setItem('ACCESS-TOKEN', result.data.access_token); | ||
| 49 | + localStorage.setItem('REFRESH-TOKEN', result.data.refresh_token); | ||
| 50 | + | ||
| 51 | + // 跳转回首页 | ||
| 52 | + window.location.href = '/index.html'; | ||
| 53 | + } | ||
| 54 | + }) | ||
| 55 | + }) | ||
| 56 | + </script> | ||
| 57 | +</head> | ||
| 58 | +<body> | ||
| 59 | +正在使用 code 授权码,进行 accessToken 访问令牌的获取 | ||
| 60 | +</body> | ||
| 61 | +</html> |
yudao-example/yudao-sso-demo-by-code/src/main/resources/static/login.html renamed to yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html
| @@ -5,8 +5,6 @@ | @@ -5,8 +5,6 @@ | ||
| 5 | <title>首页</title> | 5 | <title>首页</title> |
| 6 | <!-- jQuery:操作 dom、发起请求等 --> | 6 | <!-- jQuery:操作 dom、发起请求等 --> |
| 7 | <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> | 7 | <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/2.1.2/jquery.min.js" type="application/javascript"></script> |
| 8 | - <!-- jQuery Cookie:操作 cookie 等 --> | ||
| 9 | - <script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery-cookie/1.4.1/jquery.cookie.min.js" type="application/javascript"/></script> | ||
| 10 | 8 | ||
| 11 | <script type="application/javascript"> | 9 | <script type="application/javascript"> |
| 12 | 10 | ||
| @@ -15,7 +13,7 @@ | @@ -15,7 +13,7 @@ | ||
| 15 | */ | 13 | */ |
| 16 | function ssoLogin() { | 14 | function ssoLogin() { |
| 17 | const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId | 15 | const clientId = 'yudao-sso-demo-by-code'; // 可以改写成,你的 clientId |
| 18 | - const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback'); // 注意,需要使用 encodeURIComponent 编码地址 | 16 | + const redirectUri = encodeURIComponent('http://127.0.0.1:18080/callback.html'); // 注意,需要使用 encodeURIComponent 编码地址 |
| 19 | const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token | 17 | const responseType = 'code'; // 1)授权码模式,对应 code;2)简化模式,对应 token |
| 20 | window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId | 18 | window.location.href = 'http://127.0.0.1:1024/sso?client_id=' + clientId |
| 21 | + '&redirect_uri=' + redirectUri | 19 | + '&redirect_uri=' + redirectUri |