Commit b7b31f03d319b5322800ac462ced0f67c84e9ec9

Authored by YunaiV
1 parent 0df44b51

完成 yudao-sso-demo-by-code 实现 token 过滤器

yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java
1 1 package cn.iocoder.yudao.ssodemo.client;
2 2  
3 3 import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
4   -import cn.iocoder.yudao.ssodemo.client.dto.OAuth2AccessTokenRespDTO;
  4 +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
  5 +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
5 6 import org.springframework.core.ParameterizedTypeReference;
6 7 import org.springframework.http.*;
7 8 import org.springframework.stereotype.Component;
... ... @@ -65,6 +66,26 @@ public class OAuth2Client {
65 66 return exchange.getBody();
66 67 }
67 68  
  69 + public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) {
  70 + // 1.1 构建请求头
  71 + HttpHeaders headers = new HttpHeaders();
  72 + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
  73 + headers.set("tenant-id", TENANT_ID.toString());
  74 + addClientHeader(headers);
  75 + // 1.2 构建请求参数
  76 + MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
  77 + body.add("token", token);
  78 +
  79 + // 2. 执行请求
  80 + ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
  81 + BASE_URL + "/check-token",
  82 + HttpMethod.POST,
  83 + new HttpEntity<>(body, headers),
  84 + new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
  85 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
  86 + return exchange.getBody();
  87 + }
  88 +
68 89 private static void addClientHeader(HttpHeaders headers) {
69 90 // client 拼接,需要 BASE64 编码
70 91 String client = CLIENT_ID + ":" + CLIENT_SECRET;
... ...
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/OAuth2AccessTokenRespDTO.java renamed to yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java
1   -package cn.iocoder.yudao.ssodemo.client.dto;
  1 +package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
2 2  
3 3 import com.fasterxml.jackson.annotation.JsonProperty;
4 4 import lombok.AllArgsConstructor;
... ...
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * 校验令牌 Response DTO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class OAuth2CheckTokenRespDTO {
  19 +
  20 + /**
  21 + * 用户编号
  22 + */
  23 + @JsonProperty("user_id")
  24 + private Long userId;
  25 + /**
  26 + * 用户类型
  27 + */
  28 + @JsonProperty("user_type")
  29 + private Integer userType;
  30 + /**
  31 + * 租户编号
  32 + */
  33 + @JsonProperty("tenant_id")
  34 + private Long tenantId;
  35 +
  36 + /**
  37 + * 客户端编号
  38 + */
  39 + @JsonProperty("client_id")
  40 + private String clientId;
  41 + /**
  42 + * 授权范围
  43 + */
  44 + private List<String> scopes;
  45 +
  46 + /**
  47 + * 访问令牌
  48 + */
  49 + @JsonProperty("access_token")
  50 + private String accessToken;
  51 +
  52 + /**
  53 + * 过期时间
  54 + *
  55 + * 时间戳 / 1000,即单位:秒
  56 + */
  57 + private Long exp;
  58 +
  59 +}
... ...
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java
... ... @@ -2,7 +2,7 @@ package cn.iocoder.yudao.ssodemo.controller;
2 2  
3 3 import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
4 4 import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
5   -import cn.iocoder.yudao.ssodemo.client.dto.OAuth2AccessTokenRespDTO;
  5 +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
6 6 import org.springframework.web.bind.annotation.PostMapping;
7 7 import org.springframework.web.bind.annotation.RequestMapping;
8 8 import org.springframework.web.bind.annotation.RequestParam;
... ...
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java 0 → 100644
  1 +package cn.iocoder.yudao.ssodemo.controller;
  2 +
  3 +import org.springframework.web.bind.annotation.GetMapping;
  4 +import org.springframework.web.bind.annotation.RequestMapping;
  5 +import org.springframework.web.bind.annotation.RestController;
  6 +
  7 +@RestController
  8 +@RequestMapping("/user")
  9 +public class UserController {
  10 +
  11 + /**
  12 + * 获得当前登录用户的基本信息
  13 + *
  14 + * @return TODO
  15 + */
  16 + @GetMapping("/get")
  17 + public String getUser() {
  18 + return "";
  19 + }
  20 +
  21 +}
... ...
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/SecurityConfiguration.java renamed to yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java
1   -package cn.iocoder.yudao.ssodemo.framework;
  1 +package cn.iocoder.yudao.ssodemo.framework.config;
2 2  
  3 +import cn.iocoder.yudao.ssodemo.framework.core.TokenAuthenticationFilter;
3 4 import org.springframework.context.annotation.Configuration;
4 5 import org.springframework.http.HttpMethod;
5 6 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6 7 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  8 +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  9 +
  10 +import javax.annotation.Resource;
7 11  
8 12 @Configuration
9 13 public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
10 14  
  15 +// /**
  16 +// * Token 认证过滤器 Bean
  17 +// */
  18 +// @Bean
  19 +// public TokenAuthenticationFilter authenticationTokenFilter(OAuth2Client oauth2Client) {
  20 +// return new TokenAuthenticationFilter(oauth2Client);
  21 +// }
  22 +
  23 + @Resource
  24 + private TokenAuthenticationFilter tokenAuthenticationFilter;
  25 +
11 26 @Override
12 27 protected void configure(HttpSecurity httpSecurity) throws Exception {
  28 + // 设置 URL 安全权限
13 29 httpSecurity.csrf().disable() // 禁用 CSRF 保护
14 30 .authorizeRequests()
15 31 // 1. 静态资源,可匿名访问
... ... @@ -19,5 +35,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
19 35 // last. 兜底规则,必须认证
20 36 .and().authorizeRequests()
21 37 .anyRequest().authenticated();
  38 +
  39 + // 添加 Token Filter
  40 + httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
22 41 }
23 42 }
... ...
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java 0 → 100644
  1 +package cn.iocoder.yudao.ssodemo.framework.core;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * 登录用户信息
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class LoginUser {
  14 +
  15 + /**
  16 + * 用户编号
  17 + */
  18 + private Long id;
  19 + /**
  20 + * 用户类型
  21 + */
  22 + private Integer userType;
  23 + /**
  24 + * 租户编号
  25 + */
  26 + private Long tenantId;
  27 + /**
  28 + * 授权范围
  29 + */
  30 + private List<String> scopes;
  31 +
  32 +}
... ...
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java 0 → 100644
  1 +package cn.iocoder.yudao.ssodemo.framework.core;
  2 +
  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.oauth2.OAuth2CheckTokenRespDTO;
  6 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  7 +import org.springframework.security.core.Authentication;
  8 +import org.springframework.security.core.context.SecurityContextHolder;
  9 +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
  10 +import org.springframework.stereotype.Component;
  11 +import org.springframework.util.StringUtils;
  12 +import org.springframework.web.filter.OncePerRequestFilter;
  13 +
  14 +import javax.annotation.Resource;
  15 +import javax.servlet.FilterChain;
  16 +import javax.servlet.ServletException;
  17 +import javax.servlet.http.HttpServletRequest;
  18 +import javax.servlet.http.HttpServletResponse;
  19 +import java.io.IOException;
  20 +import java.util.Collections;
  21 +
  22 +/**
  23 + * Token 过滤器,验证 token 的有效性
  24 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +@Component
  29 +public class TokenAuthenticationFilter extends OncePerRequestFilter {
  30 +
  31 + @Resource
  32 + private OAuth2Client oauth2Client;
  33 +
  34 + @Override
  35 + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
  36 + FilterChain filterChain) throws ServletException, IOException {
  37 + // 1. 获得访问令牌
  38 + String token = obtainAuthorization(request);
  39 + if (StringUtils.hasText(token)) {
  40 + // 2. 基于 token 构建登录用户
  41 + LoginUser loginUser = buildLoginUserByToken(token);
  42 + // 3. 设置当前用户
  43 + if (loginUser != null) {
  44 + setLoginUser(loginUser, request);
  45 + }
  46 + }
  47 +
  48 + // 继续过滤链
  49 + filterChain.doFilter(request, response);
  50 + }
  51 +
  52 + private LoginUser buildLoginUserByToken(String token) {
  53 + try {
  54 + CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token);
  55 + OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData();
  56 + if (accessToken == null) {
  57 + return null;
  58 + }
  59 + // 构建登录用户
  60 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
  61 + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
  62 + } catch (Exception exception) {
  63 + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
  64 + return null;
  65 + }
  66 + }
  67 +
  68 + /**
  69 + * 从请求 Header 中,获得访问令牌
  70 + *
  71 + * @param request 请求
  72 + * @return 访问令牌
  73 + */
  74 + private static String obtainAuthorization(HttpServletRequest request) {
  75 + String authorization = request.getHeader("Authentication");
  76 + if (!StringUtils.hasText(authorization)) {
  77 + return null;
  78 + }
  79 + int index = authorization.indexOf("Bearer ");
  80 + if (index == -1) { // 未找到
  81 + return null;
  82 + }
  83 + return authorization.substring(index + 7).trim();
  84 + }
  85 +
  86 + /**
  87 + * 设置当前用户
  88 + *
  89 + * @param loginUser 登录用户
  90 + * @param request 请求
  91 + */
  92 + private static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
  93 + // 创建 Authentication,并设置到上下文
  94 + Authentication authentication = buildAuthentication(loginUser, request);
  95 + SecurityContextHolder.getContext().setAuthentication(authentication);
  96 + }
  97 +
  98 + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
  99 + // 创建 UsernamePasswordAuthenticationToken 对象
  100 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
  101 + loginUser, null, Collections.emptyList());
  102 + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
  103 + return authenticationToken;
  104 + }
  105 +
  106 +
  107 +}
... ...
yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html
... ... @@ -19,14 +19,57 @@
19 19 + '&redirect_uri=' + redirectUri
20 20 + '&response_type=' + responseType;
21 21 }
  22 +
  23 + $(function () {
  24 + const accessToken = localStorage.getItem('ACCESS-TOKEN');
  25 + // 情况一:未登录
  26 + if (!accessToken) {
  27 + $('#noLoginDiv').css("display", "block");
  28 + return;
  29 + }
  30 +
  31 + // 情况二:已登录
  32 + $('#yesLoginDiv').css("display", "block");
  33 + $('#accessTokenSpan').html(accessToken);
  34 + // 获得登录用户的信息
  35 + $.ajax({
  36 + url: "http://127.0.0.1:18080/user/get",
  37 + method: 'GET',
  38 + headers: {
  39 + 'Authentication': 'Bearer ' + accessToken
  40 + },
  41 + success: function (result) {
  42 + if (result.code !== 0) {
  43 + alert('获得个人信息失败,原因:' + result.msg)
  44 + return;
  45 + }
  46 + $('nicknameSpan').html(result.data.nickname);
  47 + }
  48 + });
  49 + })
22 50 </script>
23 51 </head>
24 52 <body>
25   -<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
26   -<div>
27   - 您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
28   -</div>
  53 + <!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
  54 + <div id="noLoginDiv" style="display: none">
  55 + 您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
  56 + </div>
29 57  
30   -<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
  58 + <!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
  59 + <div id="yesLoginDiv" style="display: none">
  60 + 您已登录!点击 <a href="#" onclick="ssoLogin()">退出 </a> 系统 <br />
  61 + 昵称:<span id="nicknameSpan"> 加载中... </span> <br />
  62 + 访问令牌:<span id="accessTokenSpan"> 加载中... </span> <br />
  63 + </div>
31 64 </body>
  65 +<style>
  66 + body { /** 页面居中 */
  67 + border-radius: 20px;
  68 + height: 350px;
  69 + position: absolute;
  70 + left: 50%;
  71 + top: 50%;
  72 + transform: translate(-50%,-50%);
  73 + }
  74 +</style>
32 75 </html>
... ...