Shrio框架Token认证思路

默认Shiro Session认证方式解析

shiro session的获取最核心的类是 DefaultWebSessionManager

默认的sesionId是分两个步骤的,第一步是从Cookie中获取,Cookie没有的的话是从Url Path中获取

Shiro Session Token认证方式

既然Shrio是从Cookie和Url中获取的SessionID,那么不适合Native手机的开发,一般会和Native开发人员定义一个Token协议,可以把Token放在Http header中,放Token有两种思路,第一种直接放入sessionId,第二种自定义Token生成然后Token和sessionId绑定.我更倾向于第二种方案. 继承类DefaultSessionManager实现自己的session获取方法.代码如下

package com.test.plats.uc.passport.client.support.session.mgt;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.WebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * @author mengxr
 * @Description: Native SessionManager
 * @date 2018/5/31 下午4:00
 */
public class LJNativeSessionManager extends DefaultSessionManager implements WebSessionManager {

    private static final Logger log = LoggerFactory.getLogger(LJNativeSessionManager.class);
    private static final String DEFAULT_TOKEN_KEY = "token_";

    public static final String accessToken = "Access-Token";
    private RedisTemplate<String, byte[]> sessionRedis;
    private int sessionTimeoutInSec = 4 * 8600;

    public RedisTemplate<String, byte[]> getSessionRedis() {
        return sessionRedis;
    }

    public LJNativeSessionManager(RedisTemplate<String, byte[]> sessionRedis) {
        this.sessionRedis = sessionRedis;
    }

    public void setSessionRedis(RedisTemplate<String, byte[]> sessionRedis) {
        this.sessionRedis = sessionRedis;
    }

    /**
     * Template method that allows subclasses to react to a new session being created.
     * <p/>
     * This method is invoked <em>before</em> any session listeners are notified.
     *
     * @param session the session that was just {@link #createSession created}.
     * @param context the {@link SessionContext SessionContext} that was used to start the session.
     */
    @Override
    protected void onStart(Session session, SessionContext context) {
        super.onStart(session, context);
        if (!WebUtils.isHttp(context)) {
            log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request " +
                    "pair. No session ID Access-Token  will be set.");
            return;
        }
        HttpServletRequest request = WebUtils.getHttpRequest(context);
        String accessToken = getAccessToken(request);
        if (null != session && null != accessToken) {
            String key = DEFAULT_TOKEN_KEY + accessToken;
            String id = session.getId().toString();
            byte[] bytes = sessionRedis.opsForValue().get(key);
            if (id != null && null == bytes || bytes.length == 0) {
                byte[] content = SerializeUtils.serialize(id);
                sessionRedis.opsForValue().set(key, content, sessionTimeoutInSec, TimeUnit.SECONDS);
            }
            if (id != null) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
                //automatically mark it valid here.  If it is invalid, the
                //onUnknownSession method below will be invoked and we'll remove the attribute at that time.
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            }
        }
    }

    /**
     * get access token
     *
     * @param request
     * @return
     */
    private String getAccessToken(HttpServletRequest request) {
        String accessToken = request.getHeader(accessToken);
        return accessToken != null ? accessToken : "";
    }

    @Override
    protected Serializable getSessionId(SessionKey sessionKey) {
        Serializable id = super.getSessionId(sessionKey);
        if (null == id && WebUtils.isWeb(sessionKey)) {
            HttpServletRequest request = WebUtils.getHttpRequest(sessionKey);
            String accessToken = getAccessToken(request);
            if (null != accessToken && accessToken.length() > 0) {
                byte[] bytes = sessionRedis.opsForValue().get(DEFAULT_TOKEN_KEY + accessToken);
                id = (Serializable) SerializeUtils.deserialize(bytes);
            }
        }
        return id;
    }

    @Override
    protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
        super.onExpiration(s, ese, key);
        onInvalidation(key);
    }

    @Override
    protected void onInvalidation(Session s, InvalidSessionException ise, SessionKey key) {
        super.onInvalidation(s, ise, key);
        onInvalidation(key);
    }

    private void onInvalidation(SessionKey key) {
        ServletRequest request = WebUtils.getRequest(key);
        if (request != null) {
            request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID);
        }
        if (WebUtils.isHttp(key)) {
            log.debug("Referenced session was invalid.  Removing session ID Access-Token.");
            HttpServletRequest httpRequest = WebUtils.getHttpRequest(key);
            String accessToken = getAccessToken(httpRequest);
            if (null != accessToken && accessToken.length() > 0) {
                sessionRedis.delete(DEFAULT_TOKEN_KEY + accessToken);
            }
        } else {
            log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
                    "pair. Session ID Access-Token will not be removed due to invalidated session.");
        }
    }

    @Override
    protected void onStop(Session session, SessionKey key) {
        super.onStop(session, key);
        if (WebUtils.isHttp(key)) {
            HttpServletRequest request = WebUtils.getHttpRequest(key);
            log.debug("Session has been stopped (subject logout or explicit stop).  Removing session ID Access-Token.");
            String accessToken = getAccessToken(request);
            if (null != accessToken && accessToken.length() > 0) {
                sessionRedis.delete(DEFAULT_TOKEN_KEY + accessToken);
            }
        } else {
            log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
                    "pair. Session ID Access-Token will not be removed due to stopped session.");
        }
    }


    /**
     * Returns {@code true} if session management and storage is managed by the underlying Servlet container or
     * {@code false} if managed by Shiro directly (called 'native' sessions).
     * <p/>
     * If sessions are enabled, Shiro can make use of Sessions to retain security information from
     * request to request.  This method indicates whether Shiro would use the Servlet container sessions to fulfill its
     * needs, or if it would use its own native session management instead (which can support enterprise features
     * - like distributed caching - in a container-independent manner).
     *
     * @return {@code true} if session management and storage is managed by the underlying Servlet container or
     * {@code false} if managed by Shiro directly (called 'native' sessions).
     */
    @Override
    public boolean isServletContainerSessions() {
        return false;
    }
}
<think>我们当前系统使用的是Apache Shiro作为安全框架,现在需要改造以支持两套独立的登录逻辑(例如普通用户和管理员)。在Shiro中,我们可以通过自定义多个Realm并配合适当的匹配策略来实现。 改造步骤: 1. 创建两个自定义Realm(用户Realm和管理员Realm) 2. 配置ModularRealmAuthenticator,并设置认证策略(AuthenticationStrategy) 3. 配置多个Realm,并指定每个Realm支持的Token类型 4. 在登录时使用不同的Token类型(用户登录使用UserToken,管理员登录使用AdminToken) 详细实现: 步骤1:创建自定义Token类 我们需要两种不同的Token,以便区分是用户登录还是管理员登录。 ```java // 用户Token public class UserToken extends UsernamePasswordToken { public UserToken(String username, String password) { super(username, password); } } // 管理员Token public class AdminToken extends UsernamePasswordToken { public AdminToken(String username, String password) { super(username, password); } } ``` 步骤2:创建两个Realm 用户Realm: ```java public class UserRealm extends AuthorizingRealm { // 指定此Realm只支持UserToken类型 @Override public boolean supports(AuthenticationToken token) { return token instanceof UserToken; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); // 根据用户名从用户系统获取用户信息 User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } // 返回认证信息,由Shiro进行密码比对 return new SimpleAuthenticationInfo( user, // 可以传入实体对象,后续在授权时使用 user.getPassword(), ByteSource.Util.bytes(user.getSalt()), // 如果有盐值 getName() ); } // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 获取当前用户 User user = (User) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 设置用户角色和权限 authorizationInfo.setRoles(userService.getRoles(user.getId())); authorizationInfo.setStringPermissions(userService.getPermissions(user.getId())); return authorizationInfo; } } ``` 管理员Realm: ```java public class AdminRealm extends AuthorizingRealm { // 指定此Realm只支持AdminToken类型 @Override public boolean supports(AuthenticationToken token) { return token instanceof AdminToken; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); Admin admin = adminService.findByUsername(username); if (admin == null) { throw new UnknownAccountException("管理员不存在"); } return new SimpleAuthenticationInfo( admin, admin.getPassword(), ByteSource.Util.bytes(admin.getSalt()), getName() ); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Admin admin = (Admin) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(adminService.getRoles(admin.getId())); authorizationInfo.setStringPermissions(adminService.getPermissions(admin.getId())); return authorizationInfo; } } ``` 步骤3:配置Shiro 在Shiro配置类中,我们需要配置多个Realm,并设置ModularRealmAuthenticator的认证策略。 ```java @Configuration public class ShiroConfig { @Bean public UserRealm userRealm() { UserRealm userRealm = new UserRealm(); // 设置凭证匹配器(如需要) userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return userRealm; } @Bean public AdminRealm adminRealm() { AdminRealm adminRealm = new AdminRealm(); adminRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return adminRealm; } // 配置多个Realm @Bean public DefaultRealmAuthenticator authenticator() { DefaultRealmAuthenticator authenticator = new DefaultRealmAuthenticator(); // 设置认证策略,一般使用AtLeastOneSuccessfulStrategy(至少一个Realm认证成功即可) // 或者AllSuccessfulStrategy(所有Realm都必须认证成功)等 authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return authenticator; } @Bean public DefaultSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置多个Realm securityManager.setAuthenticator(authenticator()); List<Realm> realms = new ArrayList<>(); realms.add(userRealm()); realms.add(adminRealm()); securityManager.setRealms(realms); return securityManager; } // 配置密码匹配器(如使用加密) @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("SHA-256"); matcher.setHashIterations(1); matcher.setStoredCredentialsHexEncoded(false); // 使用Base64编码 return matcher; } // 其他配置:ShiroFilterFactoryBean、权限注解支持等... } ``` 步骤4:在登录控制器中使用不同的Token ```java @RestController public class LoginController { @PostMapping("/user/login") public Response userLogin(@RequestBody LoginForm form) { Subject subject = SecurityUtils.getSubject(); UserToken token = new UserToken(form.getUsername(), form.getPassword()); try { subject.login(token); // 登录成功,生成自定义的响应(如返回JWT令牌或SessionId) return Response.success("登录成功"); } catch (AuthenticationException e) { return Response.fail("用户名或密码错误"); } } @PostMapping("/admin/login") public Response adminLogin(@RequestBody LoginForm form) { Subject subject = SecurityUtils.getSubject(); AdminToken token = new AdminToken(form.getUsername(), form.getPassword()); try { subject.login(token); return Response.success("管理员登录成功"); } catch (AuthenticationException e) { return Response.fail("管理员用户名或密码错误"); } } } ``` 步骤5:权限控制 在需要权限控制的地方,可以使用Shiro的注解或通过Subject进行判断。 例如,在控制器方法上添加权限注解: ```java @RequiresRoles("admin") // 要求管理员角色 @GetMapping("/admin/dashboard") public Response adminDashboard() { // ... } ``` 或者通过Subject判断: ```java Subject subject = SecurityUtils.getSubject(); if (subject.hasRole("admin")) { // 管理员操作 } ``` 关键点说明: 1. 通过不同的Token类型来区分使用哪个Realm进行认证。 2. 每个Realm的`supports`方法决定了它处理哪种Token。 3. 在ModularRealmAuthenticator中,我们设置了认证策略(如AtLeastOneSuccessfulStrategy)。由于我们每个Token只被一个Realm支持,所以实际上每次登录只会有一个Realm被调用。 4. 注意:如果多个Realm都支持同一个Token,那么会根据配置的认证策略执行多个Realm的认证。 扩展:如果管理员和用户有相同的用户名怎么办? 由于我们使用了不同的Token类型,所以即使用户名相同,也会被分发到不同的Realm进行认证,因此不会冲突。 改造后的流程: 1. 用户访问/user/login,使用UserToken进行登录,Shiro会找到UserRealm进行认证。 2. 用户访问/admin/login,使用AdminToken进行登录,Shiro会找到AdminRealm进行认证。 3. 在授权时,Shiro会根据Subject中保存的Principal(在认证时返回的)来自动选择对应的Realm进行授权(因为每个Realm返回的Principal类型不同,所以能够区分)。 注意事项: 1. 如果两个Realm返回的Principal类型相同,那么需要确保它们不会在授权时产生冲突(比如授权时根据Principal获取角色权限,要确保不会越权)。 2. 在退出登录时,调用`subject.logout()`会清除所有相关的认证和授权信息。 这样,我们就实现了在Shiro框架下支持两套独立的登录逻辑。 §§相关问题§§: 1. 在Shiro中如何实现多个Realm的认证顺序控制? 2. 如何实现Shiro的RememberMe功能并区分用户和管理员? 3. 在Shiro中如何自定义权限验证策略? 4. 如何实现Shiro的分布式会话管理?</think># 在 Shiro 中实现两套登录逻辑的改造方案 当现有系统使用 Apache Shiro 作为安全框架时,要实现两套独立的登录逻辑(如用户登录和管理员登录),可以通过以下方案进行改造: ## 核心改造思路 1. **创建两套 Realm**:分别处理用户和管理员的认证授权 2. **自定义 Token 类型**:区分用户和管理员登录请求 3. **多 Realm 认证策略**:配置 Realm 的匹配策略 4. **路径区分**:通过过滤器链定义不同的访问规则 ## 完整实现方案 ### 1. 创建自定义 Token 类型 ```java // 用户Token public class UserToken extends UsernamePasswordToken { public UserToken(String username, String password) { super(username, password); } } // 管理员Token public class AdminToken extends UsernamePasswordToken { public AdminToken(String username, String password) { super(username, password); } } ``` ### 2. 实现两套 Realm **用户 Realm**: ```java public class UserRealm extends AuthorizingRealm { // 指定此Realm只支持UserToken类型 @Override public boolean supports(AuthenticationToken token) { return token instanceof UserToken; } // 认证逻辑 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { UserToken userToken = (UserToken) token; String username = userToken.getUsername(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } return new SimpleAuthenticationInfo( user, // 认证后存入Subject的主体信息 user.getPassword(), ByteSource.Util.bytes(user.getSalt()), // 盐值 getName() ); } // 授权逻辑 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { User user = (User) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 添加用户角色和权限 info.setRoles(userService.getRoles(user.getId())); info.setStringPermissions(userService.getPermissions(user.getId())); return info; } } ``` **管理员 Realm**: ```java public class AdminRealm extends AuthorizingRealm { // 指定此Realm只支持AdminToken类型 @Override public boolean supports(AuthenticationToken token) { return token instanceof AdminToken; } // 认证逻辑 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { AdminToken adminToken = (AdminToken) token; String username = adminToken.getUsername(); Admin admin = adminService.findByUsername(username); if (admin == null) { throw new UnknownAccountException("管理员不存在"); } // 检查管理员状态 if (admin.getStatus() != AdminStatus.ACTIVE) { throw new DisabledAccountException("管理员账号已被禁用"); } return new SimpleAuthenticationInfo( admin, admin.getPassword(), ByteSource.Util.bytes(admin.getSalt()), getName() ); } // 授权逻辑 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { Admin admin = (Admin) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 添加管理员角色和权限 info.setRoles(adminService.getRoles(admin.getId())); info.setStringPermissions(adminService.getPermissions(admin.getId())); return info; } } ``` ### 3. 配置 Shiro 多 Realm 认证 ```java @Configuration public class ShiroConfig { // 用户Realm @Bean public UserRealm userRealm() { UserRealm realm = new UserRealm(); realm.setCredentialsMatcher(hashedCredentialsMatcher()); realm.setAuthorizationCachingEnabled(false); // 禁用授权缓存 return realm; } // 管理员Realm @Bean public AdminRealm adminRealm() { AdminRealm realm = new AdminRealm(); realm.setCredentialsMatcher(hashedCredentialsMatcher()); realm.setAuthorizationCachingEnabled(false); return realm; } // 配置多Realm认证器 @Bean public ModularRealmAuthenticator authenticator() { ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator(); // 设置认证策略:至少一个Realm认证成功即可 authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); return authenticator; } // 密码匹配器 @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("SHA-256"); matcher.setHashIterations(1); matcher.setStoredCredentialsHexEncoded(false); // 使用Base64编码 return matcher; } // 安全管理器 @Bean public DefaultSecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置多Realm Collection<Realm> realms = new ArrayList<>(); realms.add(userRealm()); realms.add(adminRealm()); securityManager.setRealms(realms); // 设置认证器 securityManager.setAuthenticator(authenticator()); return securityManager; } // Shiro过滤器配置 @Bean public ShiroFilterFactoryBean shiroFilter(DefaultSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); // 设置登录URL factoryBean.setLoginUrl("/login"); // 定义过滤器链 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 公开路径 filterChainDefinitionMap.put("/user/login", "anon"); filterChainDefinitionMap.put("/admin/login", "anon"); filterChainDefinitionMap.put("/static/**", "anon"); // 用户路径需要用户认证 filterChainDefinitionMap.put("/user/**", "authc, roles[USER]"); // 管理员路径需要管理员认证 filterChainDefinitionMap.put("/admin/**", "authc, roles[ADMIN]"); // 其他路径需要认证 filterChainDefinitionMap.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } } ``` ### 4. 实现登录控制器 ```java @RestController public class LoginController { @PostMapping("/user/login") public Response userLogin(@RequestBody LoginRequest request) { Subject subject = SecurityUtils.getSubject(); try { // 使用UserToken类型登录 subject.login(new UserToken( request.getUsername(), request.getPassword() )); // 获取当前用户信息 User currentUser = (User) subject.getPrincipal(); // 生成用户会话令牌 String sessionToken = generateUserSessionToken(currentUser); return Response.success("登录成功", sessionToken); } catch (AuthenticationException e) { return Response.fail("用户名或密码错误"); } } @PostMapping("/admin/login") public Response adminLogin(@RequestBody LoginRequest request) { Subject subject = SecurityUtils.getSubject(); try { // 使用AdminToken类型登录 subject.login(new AdminToken( request.getUsername(), request.getPassword() )); // 获取当前管理员信息 Admin currentAdmin = (Admin) subject.getPrincipal(); // 生成管理员会话令牌 String sessionToken = generateAdminSessionToken(currentAdmin); return Response.success("管理员登录成功", sessionToken); } catch (AuthenticationException e) { return Response.fail("管理员账号或密码错误"); } } private String generateUserSessionToken(User user) { // 生成JWT令牌的逻辑 return JwtUtils.generateToken(user.getId(), "USER"); } private String generateAdminSessionToken(Admin admin) { // 生成管理员专用JWT令牌 return JwtUtils.generateToken(admin.getId(), "ADMIN"); } } ``` ### 5. 自定义 JWT 工具类 ```java public class JwtUtils { private static final String USER_SECRET = "user-secret-key"; private static final String ADMIN_SECRET = "admin-secret-key"; private static final long EXPIRATION = 86400000; // 24小时 public static String generateToken(Long id, String type) { String secret = "USER".equals(type) ? USER_SECRET : ADMIN_SECRET; return Jwts.builder() .setSubject(id.toString()) .claim("type", type) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public static Claims parseToken(String token, String type) { String secret = "USER".equals(type) ? USER_SECRET : ADMIN_SECRET; return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } } ``` ### 6. 实现自定义过滤器(可选) 如果需要基于 JWT 进行认证,可以实现自定义过滤器: ```java public class JwtAuthFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { // 从请求头获取Token HttpServletRequest httpRequest = (HttpServletRequest) request; String token = httpRequest.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { return false; } String jwt = token.substring(7); try { // 解析Token类型 Claims claims = JwtUtils.parseToken(jwt, "USER"); Long userId = Long.parseLong(claims.getSubject()); // 验证用户状态 User user = userService.getById(userId); if (user == null || user.getStatus() != UserStatus.ACTIVE) { return false; } // 构造AuthenticationToken AuthenticationToken authToken = new UserToken( user.getUsername(), null // 密码不需要 ); // 执行Shiro登录 getSubject(request, response).login(authToken); return true; } catch (Exception e) { // 尝试管理员Token try { Claims adminClaims = JwtUtils.parseToken(jwt, "ADMIN"); Long adminId = Long.parseLong(adminClaims.getSubject()); Admin admin = adminService.getById(adminId); if (admin == null || admin.getStatus() != AdminStatus.ACTIVE) { return false; } AuthenticationToken authToken = new AdminToken( admin.getUsername(), null ); getSubject(request, response).login(authToken); return true; } catch (Exception ex) { return false; } } } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpResponse.setContentType("application/json"); httpResponse.getWriter().write("{\"code\":401,\"message\":\"未授权访问\"}"); return false; } } ``` ## 关键改造点解释 1. **Token 类型区分**: - `UserToken` 和 `AdminToken` 继承自 `UsernamePasswordToken` - Realm 通过 `supports()` 方法确定处理哪种 Token 2. **多 Realm 配置**: - 使用 `ModularRealmAuthenticator` 管理多个 Realm - 设置 `AtLeastOneSuccessfulStrategy` 认证策略 - 每个 Realm 只处理自己支持的 Token 类型 3. **权限隔离**: - 用户和管理员有不同的角色标识(USER 和 ADMIN) - 在过滤器链中定义不同的访问规则 - 授权时使用不同的权限数据源 4. **会话管理**: - 为不同类型的用户生成不同的 JWT 令牌 - 使用不同的密钥签名用户和管理员令牌 - 令牌中包含类型标识,便于后续区分 5. **安全增强**: - 禁用授权缓存,确保权限实时生效 - 检查用户/管理员状态(激活、禁用等) - 使用盐值增强密码安全性 ## 改造后登录流程 ```mermaid sequenceDiagram participant Client participant Controller participant Shiro participant UserRealm participant AdminRealm Client->>Controller: POST /user/login {username, password} Controller->>Shiro: subject.login(new UserToken(...)) Shiro->>UserRealm: 调用doGetAuthenticationInfo UserRealm->>UserService: 查询用户信息 UserService-->>UserRealm: 返回用户数据 UserRealm-->>Shiro: 返回认证信息 Shiro-->>Controller: 登录成功 Controller->>Client: 返回用户令牌 Client->>Controller: POST /admin/login {username, password} Controller->>Shiro: subject.login(new AdminToken(...)) Shiro->>AdminRealm: 调用doGetAuthenticationInfo AdminRealm->>AdminService: 查询管理员信息 AdminService-->>AdminRealm: 返回管理员数据 AdminRealm-->>Shiro: 返回认证信息 Shiro-->>Controller: 登录成功 Controller->>Client: 返回管理员令牌 ``` ## 最佳实践建议 1. **密钥管理**: - 使用不同的密钥对用户和管理员令牌签名 - 定期轮换密钥,增强安全性 - 将密钥存储在安全配置中心 2. **令牌刷新机制**: - 实现令牌刷新接口 - 设置合理的令牌有效期 - 使用刷新令牌获取新的访问令牌 3. **权限粒度控制**: - 实现细粒度的权限控制 - 支持角色继承和权限组合 - 记录权限变更日志 4. **审计日志**: - 记录所有登录事件 - 跟踪权限变更操作 - 监控异常登录行为 5. **多因素认证**: - 为管理员账户添加多因素认证 - 支持短信、邮箱、OTP等验证方式 - 记录多因素认证日志 ## 常见问题解决方案 **问题1:用户和管理员用户名冲突怎么办?** 解决方案: - 在 Realm 中明确区分用户来源 - 使用不同的数据源存储用户和管理员 - 在认证时检查用户类型 **问题2:如何实现单点登出?** 解决方案: ```java @PostMapping("/logout") public Response logout() { Subject subject = SecurityUtils.getSubject(); // 获取当前用户类型 Object principal = subject.getPrincipal(); String tokenType = principal instanceof User ? "USER" : "ADMIN"; // 登出 subject.logout(); // 将令牌加入黑名单 String token = getCurrentToken(); tokenBlacklistService.addToBlacklist(token, tokenType); return Response.success("登出成功"); } ``` **问题3:如何限制管理员只能从特定IP登录?** 在 AdminRealm 中添加: ```java @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { // ... 原有认证逻辑 // 检查IP白名单 String ip = ((AdminToken) token).getHost(); if (!adminIpWhitelist.contains(ip)) { throw new AuthenticationException("不允许从此IP登录"); } // ... 返回认证信息 } ``` **问题4:如何实现密码策略差异化?** 在 Realm 中配置不同的 CredentialsMatcher: ```java @Bean public UserRealm userRealm() { UserRealm realm = new UserRealm(); realm.setCredentialsMatcher(userCredentialsMatcher()); return realm; } @Bean public AdminRealm adminRealm() { AdminRealm realm = new AdminRealm(); realm.setCredentialsMatcher(adminCredentialsMatcher()); return realm; } @Bean public CredentialsMatcher userCredentialsMatcher() { // 用户密码策略:SHA-256,1次迭代 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("SHA-256"); matcher.setHashIterations(1); return matcher; } @Bean public CredentialsMatcher adminCredentialsMatcher() { // 管理员密码策略:更严格的策略 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("SHA-512"); matcher.setHashIterations(1024); matcher.setStoredCredentialsHexEncoded(false); return matcher; } ``` ## 总结 在 Shiro 中实现两套登录逻辑的核心改造点包括: 1. **Token 类型区分**:创建不同的 Token 类型标识登录来源 2. **多 Realm 架构**:为每种登录类型创建独立的 Realm 3. **认证策略配置**:使用 ModularRealmAuthenticator 管理多个 Realm 4. **权限隔离**:通过角色和过滤器链实现访问控制隔离 5. **会话管理**:使用不同的令牌签名密钥和类型标识 这种改造方案保持了 Shiro 的核心架构,同时实现了灵活的多认证体系,可以轻松扩展到更多登录类型(如 API 客户端、合作伙伴等)。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值