目录
2. Shiro 的核心安全管理器 SecurityManager
OAuth2Filter extends AuthenticatingFilter:
一、介绍:
shiro框架有身份验证、授权、会话管理以及加密等功能。
下面是我对身份验证(Authentication) 和授权(Authorization)功能的解析。如有错误还大家评论区斧正。
执行流程:
1. Shiro的核心组件
- Subject:代表当前用户,可以是用户、程序或第三方服务。
- SecurityManager:整个Shiro框架的核心,用于管理所有安全操作。
- Realm:数据源,负责连接实际的数据存储(如数据库、LDAP)以验证和获取用户的权限数据。
2. Shiro 的核心安全管理器 SecurityManager
@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
// 使用默认的 Web 安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义的 Realm,用于身份验证和授权
securityManager.setRealm(oAuth2Realm);
// 禁用 RememberMe 功能
securityManager.setRememberMeManager(null);
return securityManager; // 返回安全管理器
}
二、验证(Authentication):
Shiro 框架实现了基于 OAuth2 和 JWT 的身份验证流程,主要包括OAuth2Token、OAuth2Realm、OAuth2Filter、ShiroConfig和ThreadLocalToken等组件。
OAuth2Filter extends AuthenticatingFilter:
1. 拦截请求
获取请求中的token,如果token为空则返回null,否则返回一个 OAuth2Token对象。
createToken():
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String requestToken = getRequestToken(req);
if (StringUtils.isBlank(requestToken)){
return null;
}
return new OAuth2Token(requestToken);
}
OAuth2Token:
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
2. 验证token是否有效
如果token不存在,直接返回401。如果令牌过期,则到Redis当中查看是否存在令牌(这里包含令牌刷新功能,在创建token时,同时将token保存到JWT和Redis当中,并设置Redis中的token过期时间为JWT中的两倍,如果用户在JTW中token未过期时间里登录则会校验JWT中的token,如果超过这个时间就会去Redis当中查找token,如果token存在则刷新令牌,否则需要用户重新进行登录)。
如果 Token 合法,调用 executeLogin()
方法,将 Token 提交到 Shiro 的安全管理器
onAccessDenied :
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setHeader("Content-type","text/html;charset=UTF-8");
//允许跨域请求
resp.setHeader("Access-Control-Allow-Origin", resp.getHeader("Origin"));
resp.setHeader("Access-Control-Allow-Credentials", "true");
//获取请求token,如果请求里没有token,直接返回401
String token = getRequestToken(req);
try {
jwtUtil.verifyToken(token); //检查令牌是否过期
}catch (TokenExpiredException e){ //如果令牌过期,检查Redis中是否有令牌,如果存在,就重新生产一个令牌给客户端
if (redisTemplate.hasKey("token")){ //Redis中有令牌,更新令牌
redisTemplate.delete(token);//删除Redis令牌
int userId = jwtUtil.getUserId(token);
String newToken = jwtUtil.createToken(userId);//生成新的Redis令牌
redisTemplate.opsForValue().set("token", newToken);//将新令牌保存到Redis中
threadLocalToken.setToken(newToken);//将新令牌保存到Threadlocal中
}else {//Redis中没有令牌
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.getWriter().print("令牌已过期");
return false;
}
}catch (Exception e){//如果Redis不存在令牌,让用户重新登录
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.getWriter().print("无效令牌");
return false;
}
boolean bool = executeLogin(request, response);
return bool;
}
executeLogin 方法:
- executeLogin 会尝试创建认证令牌(通过 createToken 方法)并交给 Shiro 的 SecurityManager 进行认证。
- 如果认证成功,doGetAuthenticationInfo 方法会被调用以完成认证。
- 如果认证失败,executeLogin 会捕获认证异常,然后触发 onLoginFailure 方法。
onLoginFailure:
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.setHeader("Content-type","text/html;charset=UTF-8");
//允许跨域请求
resp.setHeader("Access-Control-Allow-Origin", resp.getHeader("Origin"));
resp.setHeader("Access-Control-Allow-Credentials", "true");
try {
resp.getWriter().print(e.getMessage());
} catch (IOException ex) {
}
return false;
}
3. 调用 Realm 进行认证(OAuth2Realm)
如果用户信息有效,则返回SimpleAuthenticationInfo对象,包含用户信息和token。返回的认证信息有效,Shiro 将认证视为成功。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken Token) throws AuthenticationException {
//从令牌中获取用户userId,检测用户是否被冻结
String accessToken = (String) Token.getPrincipal();
int userId = jwtUtil.getUserId(accessToken);
TbUser tbUser = userService.selectById(userId);
if (tbUser == null) {
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
// 将token信息,用户信息加入到info当中
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(tbUser, accessToken, this.getName());
return info;
}
这里有一个需要注意的点,校验完token后,通过executeLogin()将OAth2Token交给SecurityManager管理,这是SecurityManager会自动调用认证模块(触发绑定到SecurityManager的Realm,也就是OAuth2Realm,并调用其 doGetAuthenticationInfo() 方法)完成具体的认证逻辑。
三、授权(Authorization)与鉴权:
1. 在Shiro配置类当中配置权限注解支持:
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
// 设置安全管理器,用于注解支持
advisor.setSecurityManager(securityManager);
return advisor; // 返回配置好的 Advisor
}
配置好以后,当存在 @RequiresRoles、@RequiresPermissions时,就会对用户的权限进行校验,查看是否有权限使用该方法:
例:
@RequiresPermissions("user:add")
public void addUser() {
// ......
}
SecurityManager交给Realm处理,会自动调用OAuth2Realm中的doGetAuthorizationInfo()方法,获取用户的权限并返回,最终与请求的的权限进行比对。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection Collection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//查询用户权限列表
TbUser user = (TbUser) Collection.getPrimaryPrincipal();
Set<String> permissions = userService.searchUserPermissions(user.getId());
//把用户权限加入到info当中
info.setStringPermissions(permissions);
return info;
}
上面写的是关于后端的鉴权,前端的鉴权可以参考我的另一篇博客 点击前往
四、配置过滤器链
配置 Shiro 的核心过滤器工厂,用于定义过滤规则
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, OAuth2Filter oAuth2Filter) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilter.setSecurityManager(securityManager);
// 配置自定义的 OAuth2 过滤器
Map<String, Filter> filters = new HashMap<>();
filters.put("oauth2", oAuth2Filter); // 定义 oauth2 过滤器
shiroFilter.setFilters(filters);
// 配置过滤链,定义访问权限
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/user/login", "anon");
//filterMap.put("/test/**", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter; // 返回配置好的过滤器工厂
}
其中配置为“anon”的代表可以匿名访问,“oauth2”的必须经过 oAuth2Filter 处理才能访问。
具体代码实现可参考点击前往
标签:令牌,resp,Authentication,身份验证,token,Shiro,put,filterMap,Authorization From: https://blog.csdn.net/qq_67177419/article/details/144205429