<think>我们需要在同一个系统中支持两种不同的用户表进行登录认证,且不改变原有的登录方式。我们可以通过自定义Realm来实现多数据源的认证。
在Apache Shiro中,我们可以通过多个Realm来支持多个不同的用户数据源。然后使用ModularRealmAuthenticator来协调多个Realm进行认证。
同时,由于我们使用JWT,我们需要考虑如何生成和验证Token。
步骤:
1. 创建两个Realm,分别用于两种用户表的认证。
2. 配置Shiro,使其支持多个Realm,并设置认证策略(例如,任何一个Realm认证成功即可,或者所有Realm都必须成功)。
3. 由于我们使用JWT,我们需要自定义一个Token类,用于区分不同的登录方式(比如在Token中携带登录类型)。
4. 自定义一个Authenticator,根据Token中的信息选择使用哪个Realm进行认证(或者使用多个Realm尝试认证)。
5. 在登录控制器中,根据不同的登录请求(比如不同的登录接口)创建不同类型的Token。
具体实现:
第一步:定义两种Token,用于区分不同的登录方式(或者可以在同一个Token中携带一个类型字段)
我们可以扩展UsernamePasswordToken,或者使用JWT token中携带一个自定义的字段(如loginType)来区分。
但是,由于我们使用JWT,我们可能会在登录成功后生成JWT token。在登录时,我们可以通过不同的接口来区分不同的用户表,然后在生成Token时记录用户类型(或者使用不同的subject)。
这里我们采用的方式:
- 用户登录时,通过不同的登录接口(比如/user/login 和 /admin/login)来区分不同的用户表。
- 在登录控制器中,根据不同的登录接口创建不同的Token(我们可以在Token中设置一个realmName属性,这样Shiro的ModularRealmAuthenticator就可以根据这个属性来选择对应的Realm)。
第二步:创建两个Realm,分别用于认证两种用户。
每个Realm都需要实现doGetAuthenticationInfo方法,从数据库中查询对应的用户信息并返回AuthenticationInfo。
第三步:配置Shiro,将两个Realm都加入SecurityManager,并设置认证策略(这里我们使用AtLeastOneSuccessfulStrategy,即任意一个Realm认证成功即可,但实际中我们可能需要精确指定,所以最好在Token中指定使用哪个Realm,然后只让对应的Realm进行认证)。
更优的方案:自定义一个Authenticator,根据Token中的realmName来调用对应的Realm。
具体步骤:
1. 自定义Token,继承自JWT的Token(或者Shiro的AuthenticationToken),这里我们假设使用JwtToken,但登录时我们使用的是UsernamePasswordToken,所以我们可以自定义一个带登录类型的UsernamePasswordToken。
我们可以这样设计:
- 创建`UserTypeUsernamePasswordToken`,包含username、password、userType(或者realmName)。
2. 在登录接口中,根据不同的登录方式创建不同的Token:
- 接口1:创建`UserTypeUsernamePasswordToken("username", "password", "type1")`
- 接口2:创建`UserTypeUsernamePasswordToken("username", "password", "type2")`
3. 自定义一个`UserTypeModularRealmAuthenticator`,重写`doAuthenticate`方法,根据Token中的userType来选择对应的Realm。
4. 配置Shiro使用这个自定义的Authenticator。
5. 定义两个Realm,分别处理两种类型的用户认证。每个Realm需要有一个唯一的name(比如"realm1"和"realm2"),然后在Token中指定realmName为这个name。
代码示例:
第一步:自定义Token
```java
public class UserTypeUsernamePasswordToken extends UsernamePasswordToken {
private String realmName;
public UserTypeUsernamePasswordToken(String username, String password, String realmName) {
super(username, password);
this.realmName = realmName;
}
public String getRealmName() {
return realmName;
}
public void setRealmName(String realmName) {
this.realmName = realmName;
}
}
```
第二步:自定义Authenticator
```java
public class UserTypeModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
UserTypeUsernamePasswordToken token = (UserTypeUsernamePasswordToken) authenticationToken;
String realmName = token.getRealmName();
if (realmName != null) {
Realm realm = getRealm(realmName);
if (realm == null) {
throw new AuthenticationException("No Realm configured for realm name: " + realmName);
}
return doSingleRealmAuthentication(realm, token);
} else {
return doMultiRealmAuthentication(getRealms(), token);
}
}
private Realm getRealm(String realmName) {
Collection<Realm> realms = getRealms();
for (Realm realm : realms) {
if (realm.getName().equals(realmName)) {
return realm;
}
}
return null;
}
}
```
第三步:定义两个Realm,例如:
```java
public class UserRealm1 extends AuthorizingRealm {
// 设置支持的Token类型
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UserTypeUsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 授权逻辑
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UserTypeUsernamePasswordToken upToken = (UserTypeUsernamePasswordToken) token;
String username = upToken.getUsername();
// 根据用户名从第一个用户表查询用户
// 这里模拟:假设从数据库查询,如果用户不存在返回null,存在则返回AuthenticationInfo
// 实际中需要访问数据库
User user = userService1.findByUsername(username);
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
}
}
public class UserRealm2 extends AuthorizingRealm {
// 类似UserRealm1,但是访问的是另一个用户表
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UserTypeUsernamePasswordToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UserTypeUsernamePasswordToken upToken = (UserTypeUsernamePasswordToken) token;
String username = upToken.getUsername();
// 从第二个用户表查询
User2 user = userService2.findByUsername(username);
if (user == null) {
return null;
}
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
}
}
```
注意:在Realm中,我们只处理对应类型的Token。但是在上面的自定义Authenticator中,我们已经根据Token中的realmName选择了对应的Realm,所以每个Realm只需要处理自己的认证逻辑即可。
第四步:配置Shiro(使用Java配置)
```java
@Configuration
public class ShiroConfig {
@Bean
public Realm realm1() {
UserRealm1 realm = new UserRealm1();
realm.setName("realm1"); // 设置Realm的名称,与Token中的realmName对应
// 设置凭证匹配器,例如使用BCrypt
// realm.setCredentialsMatcher(hashedCredentialsMatcher());
return realm;
}
@Bean
public Realm realm2() {
UserRealm2 realm = new UserRealm2();
realm.setName("realm2");
return realm;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义的Authenticator
securityManager.setAuthenticator(new UserTypeModularRealmAuthenticator());
// 设置多个Realm
securityManager.setRealms(Arrays.asList(realm1(), realm2()));
// 其他配置...
return securityManager;
}
// 其他配置如ShiroFilterFactoryBean等
}
```
第五步:在登录控制器中,创建Token时指定realmName
```java
@RestController
public class LoginController {
@PostMapping("/user/login")
public ResponseBean login1(@RequestBody LoginForm form) {
// 创建Token,指定realmName为realm1
UserTypeUsernamePasswordToken token = new UserTypeUsernamePasswordToken(form.getUsername(), form.getPassword(), "realm1");
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
// 登录成功,生成JWT token
String jwtToken = JwtUtil.generateToken(form.getUsername());
return ResponseBean.success(jwtToken);
} catch (AuthenticationException e) {
return ResponseBean.error("登录失败");
}
}
@PostMapping("/admin/login")
public ResponseBean login2(@RequestBody LoginForm form) {
// 创建Token,指定realmName为realm2
UserTypeUsernamePasswordToken token = new UserTypeUsernamePasswordToken(form.getUsername(), form.getPassword(), "realm2");
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
String jwtToken = JwtUtil.generateToken(form.getUsername());
return ResponseBean.success(jwtToken);
} catch (AuthenticationException e) {
return ResponseBean.error("登录失败");
}
}
}
```
注意:在生成JWT token时,我们可能需要将用户类型(或realmName)也放入JWT的claims中,以便在后续的请求中验证时知道该用户属于哪个Realm(用于授权)。但是,在认证阶段,我们只需要登录成功即可。在授权阶段,我们可能需要根据用户类型来加载不同的权限。
另外,在JWT验证时,我们通常使用一个Realm来验证JWT token(因为JWT是无状态的)。所以,我们还需要一个JWT Realm来处理JWT token的认证。这样,我们的系统就同时支持两种登录方式(用户名密码和JWT),并且用户名密码又分为两种用户表。
因此,我们需要再添加一个Realm用于JWT的认证:
```java
public class JwtRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 根据JWT token中的信息(如用户名和用户类型)来获取授权信息
String username = JwtUtil.getUsername(principals.toString());
// 如何知道用户类型?可以在生成JWT token时加入用户类型,然后在解析后获取
// 这里假设我们在生成JWT token时,将用户类型作为claim放入
// 在JwtUtil中解析token时,可以获取到用户类型
// 然后根据用户类型去不同的服务获取权限
// 但是注意:在JwtRealm中,我们可能需要同时处理两种用户类型的授权,所以需要区分
// 可以在生成JWT token时,在subject或者claims中加入用户类型标识
// 例如:生成token时:JwtUtil.generateToken(username, userType)
// 解析时,可以获取到userType,然后根据userType选择不同的服务获取权限
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String jwtToken = (String) token.getCredentials();
// 验证jwtToken的合法性
if (!JwtUtil.verifyToken(jwtToken)) {
throw new AuthenticationException("Invalid token");
}
// 从JWT token中解析出用户名
String username = JwtUtil.getUsername(jwtToken);
// 这里不需要密码,因为JWT已经自包含验证信息,所以返回一个SimpleAuthenticationInfo,密码部分可以放jwtToken本身
return new SimpleAuthenticationInfo(jwtToken, jwtToken, getName());
}
}
```
这样,我们的Shiro配置中需要加入三个Realm:realm1、realm2和jwtRealm。同时,在自定义的Authenticator中,我们只处理了UserTypeUsernamePasswordToken,而JwtToken将由JwtRealm处理(因为JwtRealm的supports方法返回true)。所以,我们需要修改自定义Authenticator,使其能够处理多种Token类型,或者使用默认的ModularRealmAuthenticator(它本身支持多个Realm,并且根据Realm的supports方法选择Realm)。但是,我们之前自定义的UserTypeModularRealmAuthenticator只处理了UserTypeUsernamePasswordToken,所以我们需要调整。
调整方案:
- 我们可以不使用自定义的Authenticator,而是使用默认的ModularRealmAuthenticator,并设置认证策略(例如FirstSuccessfulStrategy)。然后,在Realm中,通过supports方法来判断是否支持当前Token。这样,对于UserTypeUsernamePasswordToken,两个Realm都会支持(因为我们在两个Realm的supports方法中都返回true),然后两个Realm都会去认证,这显然不是我们想要的。
所以,我们还是需要自定义Authenticator来根据Token中的realmName选择对应的Realm。但是,对于JwtToken,我们不希望走这个选择逻辑,而是希望由JwtRealm来处理。因此,我们可以在自定义Authenticator中,判断Token的类型,如果是JwtToken,则使用JwtRealm;如果是UserTypeUsernamePasswordToken,则根据realmName选择对应的Realm。
修改自定义Authenticator:
```java
public class UserTypeModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (authenticationToken instanceof JwtToken) {
// 如果是JwtToken,则只使用JwtRealm
return doSingleRealmAuthentication(findJwtRealm(realms), authenticationToken);
} else if (authenticationToken instanceof UserTypeUsernamePasswordToken) {
UserTypeUsernamePasswordToken token = (UserTypeUsernamePasswordToken) authenticationToken;
String realmName = token.getRealmName();
if (realmName != null) {
Realm realm = findRealmByName(realms, realmName);
if (realm == null) {
throw new AuthenticationException("No Realm configured for realm name: " + realmName);
}
return doSingleRealmAuthentication(realm, token);
} else {
return doMultiRealmAuthentication(realms, token);
}
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
private Realm findJwtRealm(Collection<Realm> realms) {
for (Realm realm : realms) {
if (realm instanceof JwtRealm) {
return realm;
}
}
throw new AuthenticationException("No JwtRealm configured");
}
private Realm findRealmByName(Collection<Realm> realms, String realmName) {
for (Realm realm : realms) {
if (realm.getName().equals(realmName)) {
return realm;
}
}
return null;
}
}
```
这样,在Shiro配置中,我们需要将三个Realm都设置进去。
注意:在JwtRealm中,我们不需要指定name,因为我们在选择时是根据类型(JwtRealm)来选择的。但是,为了统一,也可以给JwtRealm设置一个name,然后在Token中指定realmName为这个name?但是JwtToken并没有携带realmName,所以我们通过类型判断。
最后,在ShiroFilter中配置拦截规则,对于登录接口放行,其他接口需要认证。
总结:
- 我们通过自定义Token和自定义Authenticator,实现了多用户表的登录认证。
- 同时,我们支持JWT token的认证。
- 在登录接口中,通过创建不同的Token(指定realmName)来选择对应的用户表进行认证。
注意:密码加密匹配问题,两个用户表可能使用不同的加密方式,可以在各自的Realm中设置不同的CredentialsMatcher。
以上方案可以实现需求,且对原有代码改动较小。
但是,如果原有系统已经有一个Realm,那么我们需要重构,将原来的Realm拆分成两个,或者保留原来的Realm,并新增一个Realm,然后修改登录接口,让新的登录接口使用新的Realm。
如果原有系统已经有一个Realm(比如UserRealm1),那么我们需要:
- 保留UserRealm1,并修改它,使其支持UserTypeUsernamePasswordToken(在supports方法中判断)。
- 新增UserRealm2,用于第二种用户表。
- 新增JwtRealm,用于JWT认证。
- 配置自定义的Authenticator和多个Realm。
这样,原有的登录接口可能不需要改变(因为UserRealm1仍然存在),新的登录接口使用新的Realm。
如果原有系统没有使用自定义Token,那么我们需要修改登录接口,将原有的Token替换成UserTypeUsernamePasswordToken,并指定realmName(比如"realm1"),这样原有的用户认证仍然通过UserRealm1完成。
因此,这个方案可以平滑扩展。
注意:在授权部分,我们还没有实现。在授权时,我们需要根据用户类型(即来自哪个Realm)去获取不同的权限数据。这可以在Realm的doGetAuthorizationInfo方法中实现,根据Principal(用户名)和用户类型(可以在生成AuthenticationInfo时放入Principal中,例如放入一个自定义的Principal对象,包含用户名和用户类型)来获取权限。
例如:
```java
// 在UserRealm1的认证方法中
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// ... 获取用户
// 使用自定义的Principal,包含用户名和用户类型
SimplePrincipalCollection principal = new SimplePrincipalCollection();
principal.add(user.getUsername(), getName()); // 这里将Realm name作为identity的标识,也可以使用自定义对象
// 或者使用自定义的Principal对象
// 例如:UserPrincipal principal = new UserPrincipal(user.getId(), user.getUsername(), "type1");
// 然后:return new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
// 这样在授权时,doGetAuthorizationInfo的PrincipalCollection中就可以获取到这个UserPrincipal对象
return new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
}
```
在授权时,我们可以从Principal中获取用户类型,然后根据用户类型去不同的服务获取权限。
由于时间关系,这里不再展开授权部分的实现。
以上就是一个完整的扩展方案。
注意:此方案中,JWT token的生成和验证需要能够区分用户类型(在生成token时加入用户类型),以便在授权时使用。
例如,在登录成功后生成token:
```java
// 在登录控制器中,登录成功后
// 获取用户类型(根据登录的接口,或者根据Token中的realmName)
String userType = token.getRealmName(); // 这里token是UserTypeUsernamePasswordToken,其realmName就是用户类型
String jwtToken = JwtUtil.generateToken(username, userType);
```
在JwtUtil中,将userType放入claims:
```java
public static String generateToken(String username, String userType) {
// 设置过期时间
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE_TIME);
return Jwts.builder()
.setSubject(username)
.claim("userType", userType) // 加入用户类型
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
```
这样,在JwtRealm的授权方法中,我们可以从token中解析出userType,然后根据userType选择不同的权限服务。
最后,在ShiroConfig中配置好三个Realm和自定义的Authenticator,以及SecurityManager。
这样,我们就实现了两种用户表的登录,并且支持JWT认证。
注意:在ShiroFilter中,我们需要配置所有请求都需要通过JwtFilter(用于验证JWT token),而登录接口不需要。所以,我们需要一个自定义的Filter来验证JWT token。
自定义JwtFilter:
```java
public class JwtFilter extends AuthenticatingFilter {
// 这里实现JWT token的验证和登录
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String jwt = httpRequest.getHeader("Authorization");
if (jwt != null && jwt.startsWith("Bearer ")) {
jwt = jwt.substring(7);
}
return new JwtToken(jwt);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 尝试登录
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
// 跳转到登录页面或者返回错误信息
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("未授权");
out.flush();
out.close();
return false;
}
}
}
```
然后在ShiroConfig中配置Filter:
```java
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", new JwtFilter());
factoryBean.setFilters(filters);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/admin/login", "anon");
filterChainDefinitionMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
```
这样,我们就完成了整个配置。
由于问题复杂,代码量较大,以上代码仅供参考,具体实现需要根据项目调整。
最后,请注意密码加密和JWT安全相关的问题。
§§相关问题§§:
1. 在Shiro中,如何实现多个Realm的授权?
2. 如何在不同Realm中使用不同的密码加密方式?
3. 在JWT token中存储用户类型是否安全?如何增强安全性?
4. 如果需要在同一个接口中支持多种用户类型登录(比如根据用户名自动判断属于哪个用户表),应该如何修改?</think>在Spring Boot + Shiro + JWT架构中支持两种用户表的登录认证,同时保留原有登录方式,可以通过以下方案实现:
### 解决方案
1. **创建自定义Token**:区分两种登录方式
2. **实现多Realm认证**:每个Realm处理一种用户表
3. **配置多Realm策略**:使用ModularRealmAuthenticator
4. **自定义匹配器**:根据Token类型选择Realm
5. **登录接口扩展**:新增接口支持第二种登录
### 核心代码实现
#### 1. 自定义Token(添加登录类型标识)
```java
public class MultiUserToken extends UsernamePasswordToken {
private String loginType; // 标识登录类型(user1/user2)
public MultiUserToken(String username, String password, String loginType) {
super(username, password);
this.loginType = loginType;
}
// Getter
}
```
#### 2. 实现第二个Realm(处理新用户表)
```java
public class User2Realm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof MultiUserToken
&& "user2".equals(((MultiUserToken) token).getLoginType());
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String username = (String) token.getPrincipal();
// 查询第二个用户表
User2 user = user2Service.findByUsername(username);
if (user == null) throw new UnknownAccountException();
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getEncryptedPassword(),
ByteSource.Util.bytes(user.getSalt()),
getName()
);
}
// 授权方法省略...
}
```
#### 3. 修改原有Realm(添加类型检查)
```java
public class User1Realm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof MultiUserToken
&& ("user1".equals(((MultiUserToken) token).getLoginType())
|| !(token instanceof MultiUserToken)); // 兼容旧Token
}
// 原有认证逻辑保持不变...
}
```
#### 4. 配置Shiro支持多Realm
```java
@Configuration
public class ShiroConfig {
@Bean
public Realm user1Realm() {
User1Realm realm = new User1Realm();
realm.setCredentialsMatcher(new HashedCredentialsMatcher("SHA-256"));
return realm;
}
@Bean
public Realm user2Realm() {
User2Realm realm = new User2Realm();
realm.setCredentialsMatcher(new BCryptCredentialsMatcher());
return realm;
}
@Bean
public Authenticator authenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setRealms(Arrays.asList(user1Realm(), user2Realm()));
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return authenticator;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setAuthenticator(authenticator());
return manager;
}
}
```
#### 5. 新增登录控制器
```java
@RestController
public class LoginController {
// 原有登录接口(兼容旧客户端)
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
return doLogin(request.getUsername(), request.getPassword(), "user1");
}
// 新增登录接口
@PostMapping("/login/user2")
public ResponseEntity<?> loginUser2(@RequestBody LoginRequest request) {
return doLogin(request.getUsername(), request.getPassword(), "user2");
}
private ResponseEntity<?> doLogin(String username, String password, String type) {
try {
Subject subject = SecurityUtils.getSubject();
subject.login(new MultiUserToken(username, password, type));
// 生成JWT(需包含用户类型)
String jwt = JwtUtil.generateToken(username, type);
return ResponseEntity.ok(new LoginResponse(jwt));
} catch (AuthenticationException e) {
return ResponseEntity.status(401).body("认证失败");
}
}
}
```
#### 6. JWT工具类增强(携带用户类型)
```java
public class JwtUtil {
public static String generateToken(String username, String userType) {
return Jwts.builder()
.setSubject(username)
.claim("userType", userType) // 添加用户类型声明
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public static String getUserType(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
return claims.get("userType", String.class);
}
}
```
### 方案说明
1. **兼容性设计**:
- 旧登录接口继续使用`/login`路径
- 新增`/login/user2`接口处理第二种用户
- 原有Realm自动兼容旧Token格式
2. **认证流程**:
```mermaid
sequenceDiagram
客户端->>登录接口: 提交凭证(含loginType)
登录接口->>Shiro: 提交MultiUserToken
Shiro->>User1Realm: 检查是否支持Token
User1Realm-->>Shiro: 不支持(user2)
Shiro->>User2Realm: 检查是否支持Token
User2Realm->>User2Realm: 执行认证逻辑
User2Realm-->>Shiro: 返回认证结果
Shiro-->>登录接口: 返回认证状态
登录接口->>客户端: 返回JWT(含userType)
```
3. **关键优势**:
- 零修改原有用户表认证逻辑
- 清晰分离两种用户体系
- JWT携带用户类型信息
- 支持不同的加密策略(如示例中的SHA256和BCrypt)
4. **授权扩展**:
- 在`doGetAuthorizationInfo`中根据JWT中的userType加载不同权限
- 自定义注解控制不同用户类型的访问权限
### 注意事项
1. Realm的`supports()`方法必须正确实现类型检查
2. JWT中必须包含用户类型信息
3. 不同用户表建议使用不同密码加密方式
4. 授权时需根据用户类型加载对应权限
---