Commit b7b31f03d319b5322800ac462ced0f67c84e9ec9
1 parent
0df44b51
完成 yudao-sso-demo-by-code 实现 token 过滤器
Showing
9 changed files
with
311 additions
and
9 deletions
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
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> | ... | ... |