mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-10-31 10:18:42 +08:00 
			
		
		
		
	完成 yudao-sso-demo-by-code 实现 token 过滤器
This commit is contained in:
		| @@ -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,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; | ||||
| @@ -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; | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 ""; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 YunaiV
					YunaiV