依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>2.0.5</version>
</dependency>
编写配置类
@Configuration
public class ShiroConfig {
@Resource
private RedisUtils redisUtils;
@Resource
private JwtFilter jwtFilter;
private long expire = 86400000L;
public ShiroConfig() {
}
// 配置Shiro的核心安全管理器并交给bean
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(AccountRealm accountRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
ObjectMapper objectMapper = new ObjectMapper();
// 设置 Relam,告诉 shiro 要用这个relam 所联系的对象来进行权限管理 。relam 其实就可以看作 shiro 需要用来处理权限认证的相关信息的桥梁,
// 比如说这里 AccountRelam 就是把 shiro 和 AccountProfile 联系起来了
securityManager.setRealm(accountRealm);
SecurityUtils.setSecurityManager(securityManager);
// 配置缓存
ShiroCacheManager shiroCacheManager = new ShiroCacheManager();
shiroCacheManager.setCacheLiveTime(expire);
shiroCacheManager.setCacheKeyPrefix(ShiroConstant.SHIRO_AUTHORIZATION_CACHE);
shiroCacheManager.setRedisUtils(redisUtils);
shiroCacheManager.setObjectMapper(objectMapper);
// 设置缓存
securityManager.setCacheManager(shiroCacheManager);
/**
* 关闭shiro自带的session
*/
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
securityManager.setSubjectDAO(defaultSubjectDAO);
return securityManager;
}
// 配置过滤器与url之间的绑定关系
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
//加载过滤器
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilter.setFilters(filters);
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
//开启注解代理
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
编写relam
@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm {
@Resource
private JwtUtils jwtUtils;
@Resource
private UserRoleService userRoleService;
@Resource
private RoleAuthService roleAuthService;
@Resource
UserService userService;
/**
* 获取用户认证信息,执行认证逻辑
* @param jwtToken
* @return
* @throws AuthenticationException
*/
//在执行shiro 的 login 时就会执行这个方法,解析token获取uuid,进而获取当前登录用户的信息,并存储
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken jwtToken) throws AuthenticationException {
JwtToken jwt = (JwtToken) jwtToken;
String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
User user = userService.getUserInfoByUid(userId);
if(user == null){
throw new UnknownAccountException("用户不存在");
} else if(!Constants.UserStatus.NORMAL.getCode().equals(user.getStatus())){
throw new LockedAccountException("用户已被封禁或被删除,请联系管理员处理!");
}
AccountProfile accountProfile = new AccountProfile();
BeanUtil.copyProperties(user, accountProfile);
accountProfile.setUuid(userId);
return new SimpleAuthenticationInfo(accountProfile, jwt.getCredentials(), getName());
}
/**
* 获取用户权限信息,执行授权逻辑
* @param principalCollection
* @return
*/
//这个方法是在判断权限和角色的时候才会进行,具体的来说及比如说我们访问一个被 @RequiresPermissions 注解标识的handler,呢么就会执行这个方法,然后存储权限信息于缓存,如果你设置缓存的话,接着就会从缓存的权限信息中判断是否有权限执行
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
AccountProfile accountProfile = (AccountProfile) principalCollection.getPrimaryPrincipal();
// 角色权限表
List<String> permissionsNameList= new LinkedList<>();
// 用户角色表
List<String> roleNameList= new LinkedList<>();
List<Role> roleList = userRoleService.getRolesByUid(accountProfile.getId());
roleNameList = roleList.stream()
.map(Role::getName)
.collect(Collectors.toList());
for(Role role : roleList){
for(Auth auth : roleAuthService.getAuthsByRoleId(role.getId())){
permissionsNameList.add(auth.getPermission());
}
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roleNameList);
authorizationInfo.addStringPermissions(permissionsNameList);
return authorizationInfo;
}
// 注意需要支持我们 JWTtoken,
@Override
public boolean supports(AuthenticationToken token) {
// log.info("Checking token support: {}", token.getClass().getName());
return token instanceof JwtToken;
}
}
JWT_TOKEN
//需要实现 AuthenticationToken 这个接口
public class JwtToken implements AuthenticationToken{
String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return this.token;
}
@Override
public Object getCredentials() {
return this.token;
}
}
编写过滤器
// 需要继承AuthenticatingFilter
@Component
@Slf4j(topic = "haustoj")
public class JwtFilter extends AuthenticatingFilter {
@Resource
private JwtUtils jwtUtils;
// 我这里是要实现外部缓存,shiro也支持内部缓存
@Resource
private RedisUtils redisUtils;
/**
* 判断是否允许访问
*
* @param request
* @param response
* @param mappedValue
* @return
*/
//判断当前请求是否允许访问,我这里是判断是否为公开接口,如果是则通过,否则拒绝并交给 onAccessAllowed 继续处理
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpRequest = WebUtils.toHttp(request);
httpRequest.setAttribute("org.springframework.web.util.ServletRequestPathUtils.PATH",
RequestPath.parse(httpRequest.getRequestURI(), httpRequest.getContextPath()));
try {
WebApplicationContext ctx = RequestContextUtils.findWebApplicationContext(httpRequest);
RequestMappingHandlerMapping mapping = ctx.getBean(
"requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
HandlerExecutionChain handler = mapping.getHandler(httpRequest);
// 如果没有找到处理器,允许访问(由其他过滤器处理)
if (handler == null) {
return true;
}
Object handlerObj = handler.getHandler();
// 如果处理器不是HandlerMethod类型,允许访问
if (!(handlerObj instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerClazz = (HandlerMethod) handlerObj;
// 判断请求是否访问的是公共接口,如果拥有@AnonApi注解则不再走登录认证,直接访问controller对应的方法
AnonApi anonApi = ServiceContextUtils.getAnnotation(handlerClazz.getMethod(),
handlerClazz.getBeanType(),
AnonApi.class);
// 有AnonApi注解,说明无需登录就可以访问
if (anonApi != null) {
// 即使api标记了不用登录,但如果请求头携带了token,可以尝试着进行登录验证。
String jwt = httpRequest.getHeader("Authorization");
if (jwt != null && !"null".equals(jwt)) {
try {
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
// 如果已经过期,则不进行登录尝试
return true;
}
String userId = claim.getSubject();
boolean hasToken = jwtUtils.hasToken(userId);
// 缓存中不存在,说明了token失效,则不进行登录尝试
if (!hasToken) {
return true;
}
AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();
if (userRolesVo == null) {
// 尝试手动登录
JwtToken jwtToken = new JwtToken(jwt);
SecurityUtils.getSubject().login(jwtToken);
}
} catch (Exception ignored) {
// 即使出错,也不影响正常访问无鉴权接口
}
}
return true;
} else {
// 没有@AnonApi注解,需要进行认证
return false;
}
} catch (Exception e) {
// 出现异常时,默认需要认证
return false;
}
}
/**
* @param servletRequest
* @param servletResponse
* @return AuthenticationToken
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
// 获取 token
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
if (StrUtil.isBlank(jwt)) {
return null;
}
return new JwtToken(jwt);
}
// 这里就是处理非公开接口,也就是判断用户的令牌是否合法
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = httpRequest.getHeader("Authorization");
if (StrUtil.isBlank(token)) {
return this.onLoginFailure(null,
new AuthenticationException("缺少认证令牌"), request, response);
} else {
// 验证JWT token有效性
Claims claim = jwtUtils.getClaimByToken(token);
if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
return this.onLoginFailure(null,
new AuthenticationException("登录状态已失效,请重新登录!"), request, response);
}
try {
// 手动登录,这里很关键,这里就会将用户的角色、权限的等认证信息存储于缓存中
boolean loginResult = executeLogin(request, response);
Subject subject = SecurityUtils.getSubject();
log.info("认证后Subject状态 - isAuthenticated: {}, principal: {}",
subject.isAuthenticated(), subject.getPrincipal());
if (!subject.isAuthenticated()) {
log.warn("认证失败,Subject未正确设置");
return false;
}
return loginResult;
} catch (Exception e) {
log.error("认证过程中发生异常", e);
return this.onLoginFailure(null,
new AuthenticationException("认证失败"), request, response);
}
}
}
/**
* 刷新Token,并更新token到前端
*
* @param request
* @param userId
* @param response
* @return
*/
private void refreshToken(HttpServletRequest request, HttpServletResponse response, String userId) throws IOException {
String requestId = UUID.randomUUID().toString();
boolean locked = redisUtils.getLock(ShiroConstant.SHIRO_TOKEN_LOCK + userId, 20, requestId);// 获取锁20s
if (locked) {
String newToken = jwtUtils.generateToken(userId);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Authorization", newToken); //放到信息头部
response.setHeader("Access-Control-Expose-Headers", "Refresh-Token,Authorization,Url-Type"); //让前端可用访问
response.setHeader("Url-Type", request.getHeader("Url-Type")); // 为了前端能区别请求来源
response.setHeader("Refresh-Token", "true"); //告知前端需要刷新token
}
redisUtils.releaseLock(ShiroConstant.SHIRO_TOKEN_LOCK + userId, requestId);
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
returnErrorResponse(request, response, e, ResultStatus.ACCESS_DENIED);
return false;
}
private void returnErrorResponse(ServletRequest request, ServletResponse response, Exception e, ResultStatus resultStatus) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
CommonResult<Void> result = CommonResult.errorResponse(throwable.getMessage(), resultStatus);
String json = JSONUtil.toJsonStr(result);
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Expose-Headers", "Refresh-Token,Authorization,Url-Type"); //让前端可用访问
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Url-Type", httpRequest.getHeader("Url-Type")); // 为了前端能区别请求来源
httpResponse.setStatus(resultStatus.getCode());
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
httpServletResponse.setHeader("Access-Control-Expose-Headers",
"Refresh-Token,Authorization,Url-Type,Content-disposition,Content-Type"); //让前端可用访问
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
配置外部缓存(Redis)
public class ShiroCache<K,V> implements Cache<K,V> {
private Long cacheLiveTime;
private String cacheKeyPrefix;
private RedisUtils redisUtils;
private ObjectMapper objectMapper;
public ShiroCache(Long cacheLiveTime, String cacheKeyPrefix, RedisUtils redisUtils,ObjectMapper objectMapper) {
this.cacheLiveTime = cacheLiveTime;
this.cacheKeyPrefix = cacheKeyPrefix;
this.redisUtils = redisUtils;
this.objectMapper = objectMapper;
}
/**
* 缓存的key名称获取为shiro:cache:account
*
* @param key
*/
private String getKey(K key) {
String userId;
if (key instanceof PrincipalCollection) {
AccountProfile accountProfile = (AccountProfile) ((PrincipalCollection) key).getPrimaryPrincipal();
userId = accountProfile.getId();
} else {
userId = key.toString();
}
return this.cacheKeyPrefix + userId;
}
/**
* @param k
* @return V
* @throws CacheException
*/
@Override
public V get(K k) throws CacheException {
try {
V v = (V) fromJson((String) this.redisUtils.get(getKey(k)), SimpleAuthorizationInfo.class);
return v;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public V put(K k, V v) throws CacheException {
try {
String json = (String) this.toJson(v);
this.redisUtils.set(getKey(k), json, cacheLiveTime);
return v;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public V remove(K k) throws CacheException {
if(!this.redisUtils.hasKey(getKey(k))){
return null;
}
redisUtils.del(getKey(k));
return null;
}
@Override
public void clear() throws CacheException {
Set<String> keys = this.redisUtils.keys(this.cacheKeyPrefix + "*");
if (null != keys && keys.size() > 0) {
this.redisUtils.del(keys);
}
}
@Override
public int size() {
Set<String> keys = this.redisUtils.keys(this.cacheKeyPrefix + "*");
return keys.size();
}
@Override
public Set<K> keys() {
return (Set<K>) this.redisUtils.keys(this.cacheKeyPrefix + "*");
}
@Override
public Collection<V> values() {
Set<String> keys = this.redisUtils.keys(this.cacheKeyPrefix + "*");
if(CollectionUtils.isEmpty( keys)){
return Collections.emptySet();
}else{
List<Object> values = new ArrayList<>(keys.size());
Iterator iterator = keys.iterator();
while(iterator.hasNext()){
String value = (String) this.redisUtils.get((String) iterator.next());
if(value != null){
values.add(value);
}
}
return (Collection<V>) values;
}
}
// 为了让数据在redis中为json格式,不占用过多内存
private String toJson(Object obj) throws Exception {
if (obj instanceof SimpleAuthorizationInfo) {
SimpleAuthorizationInfo info = (SimpleAuthorizationInfo) obj;
Map<String, Object> map = new HashMap<>();
map.put("roles", info.getRoles());
map.put("stringPermissions", info.getStringPermissions());
map.put("objectPermissions", info.getObjectPermissions());
return objectMapper.writeValueAsString(map);
}
return objectMapper.writeValueAsString(obj);
}
private <T> T fromJson(String json, Class<T> clazz) throws Exception {
if (json == null || json.isEmpty()) {
return null; // 或者抛出业务友好的异常
}
try {
return objectMapper.readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException("JSON解析失败", e);
}
}
}
配置缓存管理
@Data
public class ShiroCacheManager implements CacheManager {
private Long cacheLiveTime;
private String cacheKeyPrefix;
private RedisUtils redisUtils;
private ObjectMapper objectMapper;
/**
* @param s
* @return Cache<K, V>
* @throws CacheException
*/
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return new ShiroCache<K, V>(cacheLiveTime, cacheKeyPrefix, redisUtils, objectMapper);
}
}
Comments NOTHING