mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-26 01:01:52 +08:00
完成 yudao-sso-demo-by-code 实现 token 过滤器
This commit is contained in:
parent
0df44b51e4
commit
b7b31f03d3
@ -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>
|
||||
<!-- 情况一:未登录:1)跳转 ruoyi-vue-pro 的 SSO 登录页 -->
|
||||
<div id="noLoginDiv" style="display: none">
|
||||
您未登录,点击 <a href="#" onclick="ssoLogin()">跳转 </a> SSO 单点登录
|
||||
</div>
|
||||
</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>
|
||||
|
Loading…
Reference in New Issue
Block a user