Преглед на файлове

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

YunaiV преди 2 години
родител
ревизия
b7b31f03d3

+ 22 - 1
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java

@@ -1,7 +1,8 @@
 package cn.iocoder.yudao.ssodemo.client;
 
 import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
-import cn.iocoder.yudao.ssodemo.client.dto.OAuth2AccessTokenRespDTO;
+import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
+import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.*;
 import org.springframework.stereotype.Component;
@@ -65,6 +66,26 @@ public class OAuth2Client {
         return exchange.getBody();
     }
 
+    public CommonResult<OAuth2CheckTokenRespDTO> checkToken(String token) {
+        // 1.1 构建请求头
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        headers.set("tenant-id", TENANT_ID.toString());
+        addClientHeader(headers);
+        // 1.2 构建请求参数
+        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
+        body.add("token", token);
+
+        // 2. 执行请求
+        ResponseEntity<CommonResult<OAuth2CheckTokenRespDTO>> exchange = restTemplate.exchange(
+                BASE_URL + "/check-token",
+                HttpMethod.POST,
+                new HttpEntity<>(body, headers),
+                new ParameterizedTypeReference<CommonResult<OAuth2CheckTokenRespDTO>>() {}); // 解决 CommonResult 的泛型丢失
+        Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功");
+        return exchange.getBody();
+    }
+
     private static void addClientHeader(HttpHeaders headers) {
         // client 拼接,需要 BASE64 编码
         String client = CLIENT_ID + ":" + CLIENT_SECRET;

+ 1 - 1
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/OAuth2AccessTokenRespDTO.java → yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.ssodemo.client.dto;
+package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.AllArgsConstructor;

+ 59 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.ssodemo.client.dto.oauth2;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 校验令牌 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OAuth2CheckTokenRespDTO {
+
+    /**
+     * 用户编号
+     */
+    @JsonProperty("user_id")
+    private Long userId;
+    /**
+     * 用户类型
+     */
+    @JsonProperty("user_type")
+    private Integer userType;
+    /**
+     * 租户编号
+     */
+    @JsonProperty("tenant_id")
+    private Long tenantId;
+
+    /**
+     * 客户端编号
+     */
+    @JsonProperty("client_id")
+    private String clientId;
+    /**
+     * 授权范围
+     */
+    private List<String> scopes;
+
+    /**
+     * 访问令牌
+     */
+    @JsonProperty("access_token")
+    private String accessToken;
+
+    /**
+     * 过期时间
+     *
+     * 时间戳 / 1000,即单位:秒
+     */
+    private Long exp;
+
+}

+ 1 - 1
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;
 
 import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
 import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
-import cn.iocoder.yudao.ssodemo.client.dto.OAuth2AccessTokenRespDTO;
+import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;

+ 21 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.ssodemo.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/user")
+public class UserController {
+
+    /**
+     * 获得当前登录用户的基本信息
+     *
+     * @return TODO
+     */
+    @GetMapping("/get")
+    public String getUser() {
+        return "";
+    }
+
+}

+ 20 - 1
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/SecurityConfiguration.java → yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java

@@ -1,15 +1,31 @@
-package cn.iocoder.yudao.ssodemo.framework;
+package cn.iocoder.yudao.ssodemo.framework.config;
 
+import cn.iocoder.yudao.ssodemo.framework.core.TokenAuthenticationFilter;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import javax.annotation.Resource;
 
 @Configuration
 public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 
+//    /**
+//     * Token 认证过滤器 Bean
+//     */
+//    @Bean
+//    public TokenAuthenticationFilter authenticationTokenFilter(OAuth2Client oauth2Client) {
+//        return new TokenAuthenticationFilter(oauth2Client);
+//    }
+
+    @Resource
+    private TokenAuthenticationFilter tokenAuthenticationFilter;
+
     @Override
     protected void configure(HttpSecurity httpSecurity) throws Exception {
+        // 设置 URL 安全权限
         httpSecurity.csrf().disable() // 禁用 CSRF 保护
                 .authorizeRequests()
                 // 1. 静态资源,可匿名访问
@@ -19,5 +35,8 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
                 // last. 兜底规则,必须认证
                 .and().authorizeRequests()
                 .anyRequest().authenticated();
+
+        // 添加 Token Filter
+        httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
     }
 }

+ 32 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.ssodemo.framework.core;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 登录用户信息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class LoginUser {
+
+    /**
+     * 用户编号
+     */
+    private Long id;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
+    /**
+     * 授权范围
+     */
+    private List<String> scopes;
+
+}

+ 107 - 0
yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java

@@ -0,0 +1,107 @@
+package cn.iocoder.yudao.ssodemo.framework.core;
+
+import cn.iocoder.yudao.ssodemo.client.OAuth2Client;
+import cn.iocoder.yudao.ssodemo.client.dto.CommonResult;
+import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.annotation.Resource;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Token 过滤器,验证 token 的有效性
+ * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
+ *
+ * @author 芋道源码
+ */
+@Component
+public class TokenAuthenticationFilter extends OncePerRequestFilter {
+
+    @Resource
+    private OAuth2Client oauth2Client;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+                                    FilterChain filterChain) throws ServletException, IOException {
+        // 1. 获得访问令牌
+        String token = obtainAuthorization(request);
+        if (StringUtils.hasText(token)) {
+            // 2. 基于 token 构建登录用户
+            LoginUser loginUser = buildLoginUserByToken(token);
+            // 3. 设置当前用户
+            if (loginUser != null) {
+                setLoginUser(loginUser, request);
+            }
+        }
+
+        // 继续过滤链
+        filterChain.doFilter(request, response);
+    }
+
+    private LoginUser buildLoginUserByToken(String token) {
+        try {
+            CommonResult<OAuth2CheckTokenRespDTO> accessTokenResult = oauth2Client.checkToken(token);
+            OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData();
+            if (accessToken == null) {
+                return null;
+            }
+            // 构建登录用户
+            return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
+                    .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
+        } catch (Exception exception) {
+            // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
+            return null;
+        }
+    }
+
+    /**
+     * 从请求 Header 中,获得访问令牌
+     *
+     * @param request 请求
+     * @return 访问令牌
+     */
+    private static String obtainAuthorization(HttpServletRequest request) {
+        String authorization = request.getHeader("Authentication");
+        if (!StringUtils.hasText(authorization)) {
+            return null;
+        }
+        int index = authorization.indexOf("Bearer ");
+        if (index == -1) { // 未找到
+            return null;
+        }
+        return authorization.substring(index + 7).trim();
+    }
+
+    /**
+     * 设置当前用户
+     *
+     * @param loginUser 登录用户
+     * @param request 请求
+     */
+    private static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
+        // 创建 Authentication,并设置到上下文
+        Authentication authentication = buildAuthentication(loginUser, request);
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+    }
+
+    private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
+        // 创建 UsernamePasswordAuthenticationToken 对象
+        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
+                loginUser, null, Collections.emptyList());
+        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+        return authenticationToken;
+    }
+
+
+}

+ 48 - 5
yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html

@@ -19,14 +19,57 @@
 				+ '&redirect_uri=' + redirectUri
 				+ '&response_type=' + responseType;
     }
+
+    $(function () {
+      const accessToken = localStorage.getItem('ACCESS-TOKEN');
+      // 情况一:未登录
+			if (!accessToken) {
+        $('#noLoginDiv').css("display", "block");
+        return;
+			}
+
+      // 情况二:已登录
+      $('#yesLoginDiv').css("display", "block");
+      $('#accessTokenSpan').html(accessToken);
+      // 获得登录用户的信息
+      $.ajax({
+        url: "http://127.0.0.1:18080/user/get",
+        method: 'GET',
+        headers: {
+          'Authentication': 'Bearer ' + accessToken
+        },
+        success: function (result) {
+          if (result.code !== 0) {
+            alert('获得个人信息失败,原因:' + result.msg)
+            return;
+          }
+          $('nicknameSpan').html(result.data.nickname);
+        }
+      });
+    })
 	</script>
 </head>
 <body>
-<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
-<div>
-	您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
-</div>
+	<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
+	<div id="noLoginDiv" style="display: none">
+		您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
+	</div>
 
-<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
+	<!-- 情况二:已登录:1)展示用户信息;2)刷新访问令牌;3)退出登录 -->
+	<div id="yesLoginDiv" style="display: none">
+		您已登录!点击 <a href="#" onclick="ssoLogin()">退出 </a> 系统 <br />
+		昵称:<span id="nicknameSpan"> 加载中... </span> <br />
+		访问令牌:<span id="accessTokenSpan"> 加载中... </span> <br />
+	</div>
 </body>
+<style>
+    body { /** 页面居中 */
+        border-radius: 20px;
+        height: 350px;
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%,-50%);
+    }
+</style>
 </html>