diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java index 351224bf0..8449dd39b 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java +++ b/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 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 body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/check-token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + private static void addClientHeader(HttpHeaders headers) { // client 拼接,需要 BASE64 编码 String client = CLIENT_ID + ":" + CLIENT_SECRET; diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/OAuth2AccessTokenRespDTO.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java similarity index 93% rename from yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/OAuth2AccessTokenRespDTO.java rename to yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java index 14c01b843..6a5369a20 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/OAuth2AccessTokenRespDTO.java +++ b/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; diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java new file mode 100644 index 000000000..862bcf04b --- /dev/null +++ b/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 scopes; + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 过期时间 + * + * 时间戳 / 1000,即单位:秒 + */ + private Long exp; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java index 8ed601ba5..21807170e 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/AuthController.java +++ b/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; diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java new file mode 100644 index 000000000..77e4a9113 --- /dev/null +++ b/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 ""; + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/SecurityConfiguration.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java similarity index 57% rename from yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/SecurityConfiguration.java rename to yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java index 608191350..3a27f0102 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/SecurityConfiguration.java +++ b/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); } } diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/LoginUser.java new file mode 100644 index 000000000..e785e1544 --- /dev/null +++ b/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 scopes; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java new file mode 100644 index 000000000..a895fc2c5 --- /dev/null +++ b/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 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; + } + + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html b/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html index 2163a466c..df037f4c5 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html +++ b/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); + } + }); + }) - -
- 您未登录,点击 跳转 SSO 单点登录 -
+ + - + + +