permissions;
+}
diff --git a/src/main/java/cn/teammodel/security/SecurityConfiguration.java b/src/main/java/cn/teammodel/security/SecurityConfiguration.java
index 32fb123..a5b3b60 100644
--- a/src/main/java/cn/teammodel/security/SecurityConfiguration.java
+++ b/src/main/java/cn/teammodel/security/SecurityConfiguration.java
@@ -3,6 +3,7 @@ package cn.teammodel.security;
import cn.teammodel.security.filter.AuthInnerTokenFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
@@ -20,6 +21,7 @@ import javax.annotation.Resource;
@Configuration
@EnableWebSecurity
+@EnableMethodSecurity
public class SecurityConfiguration {
@Resource
private AuthInnerTokenFilter authInnerTokenFilter;
diff --git a/src/main/java/cn/teammodel/security/filter/AuthInnerTokenFilter.java b/src/main/java/cn/teammodel/security/filter/AuthInnerTokenFilter.java
index b1f6eb4..6a61b1d 100644
--- a/src/main/java/cn/teammodel/security/filter/AuthInnerTokenFilter.java
+++ b/src/main/java/cn/teammodel/security/filter/AuthInnerTokenFilter.java
@@ -1,5 +1,17 @@
package cn.teammodel.security.filter;
+import cn.teammodel.config.exception.ServiceException;
+import cn.teammodel.model.entity.TmdUserDetail;
+import cn.teammodel.security.utils.JwtTokenUtil;
+import cn.teammodel.security.utils.SecurityUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -8,6 +20,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
+import java.util.Collection;
/**
* x-auth-authToken filter
@@ -15,9 +28,28 @@ import java.io.IOException;
* @create 2023-11-09 10:43
*/
@Component
+@Slf4j
public class AuthInnerTokenFilter extends OncePerRequestFilter {
+
+ @Autowired
+ JwtTokenUtil jwtTokenUtil;
+
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ SecurityContext context = SecurityContextHolder.getContext();
+ Authentication authentication = context.getAuthentication();
+ // 进入此过滤器说明 OIDC 认证成功,则验证 authToken
+ // 验证 authToken 合法
+ TmdUserDetail tmdUserDetail = jwtTokenUtil.getValidUserDetail(request);
+ if (tmdUserDetail == null) {
+ log.error("authToken authentication failed");
+ throw new ServiceException("无权限");
+ }
+ System.out.println(tmdUserDetail.getUser());
+ // 组装 authToken 的 jwt 进 authentication
+ Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
+ UsernamePasswordAuthenticationToken finalAuthentication = new UsernamePasswordAuthenticationToken(tmdUserDetail, null, authorities);
+ context.setAuthentication(finalAuthentication);
filterChain.doFilter(request, response);
}
}
diff --git a/src/main/java/cn/teammodel/security/service/PermissionService.java b/src/main/java/cn/teammodel/security/service/PermissionService.java
new file mode 100644
index 0000000..ddaad19
--- /dev/null
+++ b/src/main/java/cn/teammodel/security/service/PermissionService.java
@@ -0,0 +1,174 @@
+package cn.teammodel.security.service;
+
+import java.util.Set;
+
+import cn.teammodel.model.entity.User;
+import cn.teammodel.security.utils.SecurityUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * 自定义权限实现,ss取自SpringSecurity首字母
+ *
+ * 1. IES 类似角色判断: hasAuth
+ * 2. role 身份判断(authToken 解析出来的用户身份): hasRole
+ * 3. permission 权限判断(authToken 解析出来的用户权限): hasPermi
+ *
+ * @author ruoyi
+ */
+@Service("ss")
+public class PermissionService
+{
+ /** 所有权限标识 */
+ private static final String ALL_PERMISSION = "*:*:*";
+
+ /** 管理员角色权限标识 */
+ private static final String SUPER_ADMIN = "admin";
+
+ private static final String ROLE_DELIMETER = ",";
+
+ private static final String PERMISSION_DELIMETER = ",";
+
+
+
+
+ /**
+ * access_token 是否拥有 auth 角色
+ * @param auth:
+ * @return: boolean
+ * @author: winter
+ * @date: 2023/11/10 10:05
+ * @description:
+ */
+ public boolean hasAuth(String auth)
+ {
+ if (StringUtils.isEmpty(auth))
+ {
+ return false;
+ }
+ Authentication authentication = SecurityUtils.getAuthentication();
+ if (authentication == null || CollectionUtils.isEmpty(authentication.getAuthorities()))
+ {
+ return false;
+ }
+ return authentication.getAuthorities().contains(auth);
+ }
+
+ /**
+ * 验证用户是否具备某权限
+ *
+ * @param permission 权限字符串
+ * @return 用户是否具备某权限
+ */
+ public boolean hasPermi(String permission)
+ {
+ if (StringUtils.isEmpty(permission))
+ {
+ return false;
+ }
+ User loginUser = SecurityUtils.getLoginUser();
+ if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions()))
+ {
+ return false;
+ }
+ return loginUser.getPermissions().contains(permission);
+ }
+
+ /**
+ * 验证用户是否不具备某权限,与 hasPermi逻辑相反
+ *
+ * @param permission 权限字符串
+ * @return 用户是否不具备某权限
+ */
+ public boolean lacksPermi(String permission)
+ {
+ return !hasPermi(permission);
+ }
+
+ /**
+ * 验证用户是否具有以下任意一个权限
+ *
+ * @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表
+ * @return 用户是否具有以下任意一个权限
+ */
+ public boolean hasAnyPermi(String permissions)
+ {
+ if (StringUtils.isEmpty(permissions))
+ {
+ return false;
+ }
+ User loginUser = SecurityUtils.getLoginUser();
+ if (loginUser == null || CollectionUtils.isEmpty(loginUser.getPermissions()))
+ {
+ return false;
+ }
+ Set authorities = loginUser.getPermissions();
+ for (String permission : permissions.split(PERMISSION_DELIMETER))
+ {
+ if (permission != null && authorities.contains(permission))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断用户是否拥有某个角色
+ *
+ * @param role 角色字符串
+ * @return 用户是否具备某角色
+ */
+ public boolean hasRole(String role)
+ {
+ if (StringUtils.isEmpty(role)) {
+ return false;
+ }
+ User loginUser = SecurityUtils.getLoginUser();
+ if (loginUser == null || CollectionUtils.isEmpty(loginUser.getRoles()))
+ {
+ return false;
+ }
+ return loginUser.getRoles().contains(role);
+ }
+
+ /**
+ * 验证用户是否不具备某角色,与 isRole逻辑相反。
+ *
+ * @param role 角色名称
+ * @return 用户是否不具备某角色
+ */
+ public boolean lacksRole(String role)
+ {
+ return !hasRole(role);
+ }
+
+ /**
+ * 验证用户是否具有以下任意一个角色
+ *
+ * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
+ * @return 用户是否具有以下任意一个角色
+ */
+ public boolean hasAnyRoles(String roles)
+ {
+ if (StringUtils.isEmpty(roles))
+ {
+ return false;
+ }
+ User loginUser = SecurityUtils.getLoginUser();
+ if (loginUser == null || CollectionUtils.isEmpty(loginUser.getRoles()))
+ {
+ return false;
+ }
+ for (String role : roles.split(ROLE_DELIMETER))
+ {
+ if (hasRole(role))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/cn/teammodel/security/utils/JwtTokenUtil.java b/src/main/java/cn/teammodel/security/utils/JwtTokenUtil.java
index 16bb8c1..32c0e95 100644
--- a/src/main/java/cn/teammodel/security/utils/JwtTokenUtil.java
+++ b/src/main/java/cn/teammodel/security/utils/JwtTokenUtil.java
@@ -1,16 +1,21 @@
package cn.teammodel.security.utils;
+import cn.teammodel.model.entity.User;
+import cn.teammodel.model.entity.TmdUserDetail;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
+import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
/**
* @author winter
@@ -25,23 +30,21 @@ public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
- @Value("${jwt.expiration}")
- private Long expiration;
- @Value("${jwt.tokenHead}")
- private String tokenHead;
+ private Integer expiration = 30;
/**
* 生成token
* @param userDetails 传入userDetails
* @return token
*/
+ @Deprecated
public String generateToken(UserDetails userDetails){
Map claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED,new Date());
return generateToken(claims);
}
-
+ @Deprecated
private String generateToken(Map claims){
return Jwts.builder()
.setClaims(claims)
@@ -63,6 +66,7 @@ public class JwtTokenUtil {
* @param token 前端传入的token
* @return 负载中的用户名,获取失败返回null
*/
+ @Deprecated
public String getUsernameFromToken(String token) {
String username = null;
try {
@@ -79,15 +83,14 @@ public class JwtTokenUtil {
* @param token token
* @return claims或者null
*/
- private Claims getClaimsFromToken(String token){
+ private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
- .setSigningKey(secret)
+ .setSigningKey(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"))
.parseClaimsJws(token)
.getBody();
- } catch (Exception e){
- e.printStackTrace();
+ } catch (Exception e) {
log.info("jwt解析出错:{}",token);
}
return claims;
@@ -96,13 +99,21 @@ public class JwtTokenUtil {
/**
* 检验token是否还有效
* @param token 前端传入的token
- * @param userDetails 登录用户细节
* @return true表示有效,false表示无效
*/
public boolean validateToken(String token) {
return !isTokenExpired(token);
}
+ public boolean validateToken(HttpServletRequest request) {
+ String token = request.getHeader("x-auth-AuthToken");
+ if (StringUtils.isBlank(token)) {
+ return false;
+ }
+
+ return validateToken(token);
+ }
+
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
@@ -112,4 +123,60 @@ public class JwtTokenUtil {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
+
+ /**
+ * token 验证成功返回实体,验证失败返回 null
+ * @param request:
+ * @return: cn.teammodel.model.entity.TmdUserDetail
+ * @author: winter
+ * @date: 2023/11/10 10:24
+ * @description:
+ */
+ public TmdUserDetail getValidUserDetail(HttpServletRequest request) {
+ String token = request.getHeader("x-auth-AuthToken");
+ if (StringUtils.isBlank(token)) {
+ return null;
+ }
+ Claims claims = getClaimsFromToken(token);
+ if (claims == null) {
+ return null;
+ }
+
+ // 组装 TmdUserDetail
+ TmdUserDetail tmdUserDetail = new TmdUserDetail();
+ User user = new User();
+ String id = claims.getSubject();
+ user.setId(id);
+ user.setName(claims.get("name") == null ? null : claims.get("name", String.class));
+ user.setPicture(claims.get("picture") == null ? null : claims.get("picture", String.class));
+ user.setStandard(claims.get("standard") == null ? null : claims.get("standard", String.class));
+ user.setScope(claims.get("scope") == null ? null : claims.get("scope", String.class));
+ user.setWebsite(claims.get("website") == null ? null : claims.get("website", String.class));
+ user.setArea(claims.get("area") == null ? null : claims.get("area", String.class));
+
+ // 取出 roles 和 permissions
+ Set roleSet = convertToArray(claims.get("roles"));
+ Set permissionSet = convertToArray(claims.get("permissions"));
+ user.setRoles(roleSet);
+ user.setPermissions(permissionSet);
+ tmdUserDetail.setUser(user);
+ return tmdUserDetail;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Set convertToArray(Object o) {
+ if (o == null) {
+ return Collections.emptySet();
+ }
+ if (o instanceof String) {
+ if (StringUtils.isNotBlank((String)o)) {
+ return Arrays.stream(((String) o).split(" ")).collect(Collectors.toSet());
+ }
+ return Collections.emptySet();
+ }
+ if (o instanceof Collection) {
+ return new HashSet<>(((Collection) o));
+ }
+ return Collections.emptySet();
+ }
}
\ No newline at end of file
diff --git a/src/main/java/cn/teammodel/security/utils/SecurityUtils.java b/src/main/java/cn/teammodel/security/utils/SecurityUtils.java
new file mode 100644
index 0000000..a667415
--- /dev/null
+++ b/src/main/java/cn/teammodel/security/utils/SecurityUtils.java
@@ -0,0 +1,107 @@
+package cn.teammodel.security.utils;
+
+import cn.teammodel.config.exception.ServiceException;
+import cn.teammodel.model.entity.User;
+import cn.teammodel.model.entity.TmdUserDetail;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+/**
+ * 安全服务工具类
+ *
+ * @author winter
+ */
+public class SecurityUtils
+{
+ /**
+ * 用户ID
+ **/
+ public static String getUserId()
+ {
+ try
+ {
+ return getLoginUser().getId();
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("获取用户ID异常", HttpStatus.UNAUTHORIZED.value());
+ }
+ }
+
+
+ /**
+ * 获取用户账户
+ **/
+ public static String getUsername()
+ {
+ try
+ {
+ return getLoginUser().getName();
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("获取用户账户异常", HttpStatus.UNAUTHORIZED.value());
+ }
+ }
+
+ /**
+ * 获取用户
+ **/
+ public static User getLoginUser()
+ {
+ try
+ {
+ return ((TmdUserDetail) getAuthentication().getPrincipal()).getUser();
+ }
+ catch (Exception e)
+ {
+ throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED.value());
+ }
+ }
+
+ /**
+ * 获取Authentication
+ */
+ public static Authentication getAuthentication()
+ {
+ return SecurityContextHolder.getContext().getAuthentication();
+ }
+
+ /**
+ * 生成BCryptPasswordEncoder密码
+ *
+ * @param password 密码
+ * @return 加密字符串
+ */
+ public static String encryptPassword(String password)
+ {
+ BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+ return passwordEncoder.encode(password);
+ }
+
+ /**
+ * 判断密码是否相同
+ *
+ * @param rawPassword 真实密码
+ * @param encodedPassword 加密后字符
+ * @return 结果
+ */
+ public static boolean matchesPassword(String rawPassword, String encodedPassword)
+ {
+ BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+ return passwordEncoder.matches(rawPassword, encodedPassword);
+ }
+
+ /**
+ * 是否为管理员
+ *
+ * @param userId 用户ID
+ * @return 结果
+ */
+ public static boolean isAdmin(Long userId)
+ {
+ return userId != null && 1L == userId;
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 93c87b7..74b1bdd 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -11,4 +11,7 @@ spring:
resourceserver:
jwt:
issuer-uri: https://login.partner.microsoftonline.cn/4807e9cf-87b8-4174-aa5b-e76497d7392b/v2.0
- audiences: 72643704-b2e7-4b26-b881-bd5865e7a7a5
\ No newline at end of file
+ audiences: 72643704-b2e7-4b26-b881-bd5865e7a7a5
+
+jwt:
+ secret: fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA=
\ No newline at end of file
diff --git a/src/test/java/cn/teammodel/DeploymentTestApplicationTests.java b/src/test/java/cn/teammodel/TeamModelExtensionApplicationTests.java
similarity index 80%
rename from src/test/java/cn/teammodel/DeploymentTestApplicationTests.java
rename to src/test/java/cn/teammodel/TeamModelExtensionApplicationTests.java
index 2600be7..a178f89 100644
--- a/src/test/java/cn/teammodel/DeploymentTestApplicationTests.java
+++ b/src/test/java/cn/teammodel/TeamModelExtensionApplicationTests.java
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
-class DeploymentTestApplicationTests {
+class TeamModelExtensionApplicationTests {
@Test
void contextLoads() {
diff --git a/src/test/java/cn/teammodel/TestWithoutSpring.java b/src/test/java/cn/teammodel/TestWithoutSpring.java
new file mode 100644
index 0000000..484bc84
--- /dev/null
+++ b/src/test/java/cn/teammodel/TestWithoutSpring.java
@@ -0,0 +1,25 @@
+package cn.teammodel;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author winter
+ * @create 2023-11-10 10:42
+ */
+public class TestWithoutSpring {
+ @Test
+ public void testJwtDecode() {
+// Claims claims = null;
+// try {
+// Jwts.parser()
+//
+//
+//// claims = Jwts.parser()
+//// .setSigningKey("fXO6ko/qyXeYrkecPeKdgXnuLXf9vMEtnBC9OB3s+aA=")
+//// .parseClaimsJws("eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobWFjLXNoYTI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0LnRlYW1tb2RlbC5jbiIsInN1YiI6IjE1OTUzMjEzNTQiLCJhenAiOiJoYmNuIiwiZXhwIjoxNjk5NTg2MzM1LCJuYW1lIjoi572X6ICB5biIIiwicGljdHVyZSI6Imh0dHBzOi8vY29yZXN0b3JhZ2VzZXJ2aWNlLmJsb2IuY29yZS5jaGluYWNsb3VkYXBpLmNuL2FjY291bnQvYXZhdGFyLzE1OTUzMjEzNTQiLCJyb2xlcyI6WyJ0ZWFjaGVyIiwiYWRtaW4iLCJhcmVhIl0sInBlcm1pc3Npb25zIjpbXSwic3RhbmRhcmQiOiJzdGFuZGFyZDEiLCJzY29wZSI6InRlYWNoZXIiLCJhcmVhIjoiNjllM2Q0MTMtNTBhMS00ZjVlLTg0NGEtZTBmN2M5NjIyZWEzIiwid2Vic2l0ZSI6IklFUyJ9.4NdqwDdDQcyberEAirX0srOIb1ADXLJfP-a9qNXb0yw")
+//// .getBody();
+// } catch (Exception e) {
+// e.printStackTrace();
+// }
+ }
+}