基本概念
不废话,干代码使用shiro需要一个配置类,我这边是用了springboot。搞了个bean;shiro的三个重要配置
1、Realm:定义自己逻辑,比如这个地方你可去关联你的userService去查询个人角色信息,给框架返回一个角色列表。
2、DefaultWebSecurityManager:可AI查下概念,我的解读是为了我可以使用ShiroFilterFactoryBean定义我的url而存在。
3、ShiroFilterFactoryBean:可以根据url去走过滤器,从而实现控制权限。
JwtFilter.preHandle
解:对跨域提供支持,过滤器链中拦截请求,判断是否为跨域请求
JwtFilter.isAccessAllowed
解:判断是否携带了有效的JwtToken
JwtFilter.executeLogin
解:执行登陆,从Header获取token
getSubject
解:传入到AccountRealm方便做验证动作
AccountRealm.supports
解:标识这个Realm是专门用来验证JwtToken,不负责验证其他的token(UsernamePasswordToken)
AccountRealm.doGetAuthenticationInfo
解:
1:获取jwt中关于用户名
2: 判断token是否过期与合法性
3: 放入SimpleAuthenticationInfo,便于下个对象获取
AccountRealm.doGetAuthorizationInfo
解:获取上个方法传递的参数,进行权限分类(不想每次都去查db,于是引入了redis)
ShiroConfig
package com.chinaratings.config;
import com.chinaratings.enums.AccountRealm;
import com.chinaratings.jwt.JwtFilter;
import jakarta.servlet.Filter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import java.util.HashMap;
import java.util.LinkedHashMap;
/**
* shiro启用注解拦截控制器
* shiro的三个重要配置
* 1、Realm
* 2、DefaultWebSecurityManager
* 3、ShiroFilterFactoryBean
*/
@Configuration
public class ShiroConfig {
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(@Qualifier("accountRealm") AccountRealm accountRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(accountRealm);
// 关闭shiroDao功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将ShiroSession中的东西存到任何地方包括Http Session中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
// securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
shiroFilter.setFilters(getFilters());
shiroFilter.setFilterChainDefinitionMap(getFilterChainDefinitionMap());
return shiroFilter;
}
// 提取过滤器链配置到单独方法
private LinkedHashMap<String, String> getFilterChainDefinitionMap() {
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 添加anon路径
addAnonPaths(filterChainDefinitionMap);
// 添加需要认证的路径
addAuthPaths(filterChainDefinitionMap);
return filterChainDefinitionMap;
}
// 添加所有anon路径
private void addAnonPaths(LinkedHashMap<String, String> filterChainDefinitionMap) {
filterChainDefinitionMap.put("/user/login*", "anon");
filterChainDefinitionMap.put("/validatecode.jsp*", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/authcode", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/csrf", "anon");
filterChainDefinitionMap.put("/user/verificationCode", "anon");
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/user/forgotPassword", "anon");
}
// 添加需要认证的路径
private void addAuthPaths(LinkedHashMap<String, String> filterChainDefinitionMap) {
//filterChainDefinitionMap.put("/ims/webFile/**", "roles[admin]");
///**作为备选路径,确保所有其他路径都经过jwt过滤器
filterChainDefinitionMap.put("/**", "jwt");
}
// 提取过滤器配置到单独方法
private HashMap<String, Filter> getFilters() {
HashMap<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt", new JwtFilter());
// 确保JwtFilter中有完善的异常处理
return filterMap;
}
/**
* 解决@RequiresAuthentication注解不生效的配置
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 为Spring-Bean开启对Shiro注解的支持
*/
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
AccountRealm
package com.chinaratings.enums;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.chinaratings.config.BaseException;
import com.chinaratings.ims.dto.ResultDTO;
import com.chinaratings.ims.dto.User;
import com.chinaratings.ims.service.UserService;
import com.chinaratings.ims.vo.UserVo;
import com.chinaratings.jwt.JwtToken;
import com.chinaratings.jwt.JwtUtil;
import com.chinaratings.util.RedisUtil;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
@Component
public class AccountRealm extends AuthorizingRealm {
@Resource
private JwtUtil jwtUtil;
@Autowired
private UserService userService;
@Autowired
private RedisUtil redisUtil;
/**
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String jwt = (String) authenticationToken.getCredentials();
// 获取jwt中关于用户名
String username = jwtUtil.getClaimsByToken(jwt).getSubject();
// 查询用户
Object obj = redisUtil.get(username);
if(null == obj){
throw new BaseException(ResponseCodeEnum.LOGIN_ERROR, "未登录或Token失效");
}
// 将JSON字符串转换为用户对象
UserVo userVo = JSON.parseObject((String)obj, UserVo.class);
Claims claims = jwtUtil.getClaimsByToken(jwt);
if (jwtUtil.isTokenExpired(claims.getExpiration())) {
throw new BaseException(ResponseCodeEnum.LOGIN_ERROR, "token过期,请重新登录");
}
return new SimpleAuthenticationInfo(userVo, jwt, getName());
}
/**
* 授权
* 权限分类:
* 1:管理员 访问所有资源
* 2:vip 除管理模块外均可访问
* 3:normal : 不能下载,不能翻页,不能访问管理模块
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
UserVo userVo = (UserVo)principalCollection.getPrimaryPrincipal();
Set<String> permsSet = new HashSet<>();
permsSet.add(userVo.getRoleName());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(permsSet);
return info;
}
}
JwtFilter
package com.chinaratings.jwt;
import com.alibaba.fastjson.JSON;
import com.chinaratings.enums.ResponseCodeEnum;
import com.chinaratings.ims.dto.ResultDTO;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import java.io.IOException;
@Slf4j
@Component
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* isAccessAllowed()判断是否携带了有效的JwtToken
* onAccessDenied()是没有携带JwtToken的时候进行账号密码登录,登录成功允许访问,登录失败拒绝访问
* * 1. 返回true,shiro就直接允许访问url
* * 2. 返回false,shiro才会根据onAccessDenied的方法的返回值决定是否允许访问url
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
try {
return executeLogin(servletRequest, servletResponse);
} catch (Exception e) {
return false;
}
}
/**
* 执行登录
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response){
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(JwtUtil.HEADER);
if (StringUtils.isEmpty(token)) {
return true;
}
JwtToken jwtToken = new JwtToken(token);
getSubject(request, response).login(jwtToken);
return true;
}
/**
* @param servletRequest
* @param servletResponse
* @throws Exception
* @return 返回结果为true表明登录通过
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
httpServletResponse.setStatus(ResponseCodeEnum.OK.getCode());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("applicatioin/json; charset=utf-8");
try(ServletOutputStream out = httpServletResponse.getOutputStream()){
out.write(JSON.toJSONString(ResultDTO.error(ResponseCodeEnum.TOKEN_ERROR)).getBytes("utf-8"));
}catch(IOException e) {
throw new AuthenticationException("直接返回Response信息出现IOException异常:" + e.getMessage());
}
return false;
}
/**
* 对跨域提供支持
* 过滤器链中拦截请求,判断是否为跨域请求
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 判断请求方式是否为跨域请求
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", httpServletRequest.getHeader("Access-Control-Request-Method"));
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
return false;
}
// 继续执行过滤器链
return super.preHandle(request, response);
}
}
JwtToken
package com.chinaratings.jwt;
import org.apache.shiro.authc.AuthenticationToken;
/**
* 继承AuthenticationToken,跟AccountRealmh中的doGetAuthenticationInfo的参数类型保持一致
*/
public class JwtToken implements AuthenticationToken {
private String username;
private String token;
public JwtToken(String token){
this.token = token;
JwtUtil jwtUtil = new JwtUtil();
this.username = jwtUtil.getClaimFiled(token, "username");
}
/**
* 类似用户名
* @return
*/
@Override
public Object getPrincipal() {
return username;
}
/**
* 类似密码
* @return
*/
@Override
public Object getCredentials() {
return token;
}
}
JwtUtil
package com.chinaratings.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
@Component
@Slf4j
public class JwtUtil {
private static final String SECRET = "zxcvbnmfdasaererafafafafafafakjlkjalkfafadffdafadfafafaaafadfadfaf1234567890";
private static final long EXPIRE = 60 * 24 * 7;
public static final String HEADER = "Authorization";
/**
* 生成jwt token
*/
public String generateToken(String username) {
SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
//过期时间
LocalDateTime tokenExpirationTime = LocalDateTime.now().plusMinutes(EXPIRE);
return Jwts.builder()
.signWith(signingKey, Jwts.SIG.HS512)
.header().add("typ", "JWT").and()
.issuedAt(Timestamp.valueOf(LocalDateTime.now()))
.subject(username)
.expiration(Timestamp.valueOf(tokenExpirationTime))
.claims(Map.of("username", username))
.compact();
}
public Claims getClaimsByToken(String token) {
SecretKey signingKey = Keys.hmacShaKeyFor(SECRET.getBytes(StandardCharsets.UTF_8));
return Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 检查token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
/**
* 获得token中的自定义信息,一般是获取token的username,无需secret解密也能获得
* @param token
* @param filed
* @return
*/
public String getClaimFiled(String token, String filed){
try{
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(filed).asString();
} catch (JWTDecodeException e){
log.error("JwtUtil getClaimFiled error: ", e);
return null;
}
}
}