自定义过滤器,重写
isAccessAllowed
自定义realm,重写
doGetAuthorizationInfo
我需要根据链接参数不同进行授权,就是通过这种方法的。
public class CustomRealm extends AuthorizingRealm
下面是授权代码
/**
* 获取权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("开始鉴权------------doGetAuthorizationInfo");
User user = (User) principalCollection.getPrimaryPrincipal();
List userRoles = new ArrayList();
Set categoryIds = new HashSet<>();
QueryWrapper queryUserWrapper = new QueryWrapper();
queryUserWrapper.eq("uuid",user.getUuid());
user = userService.getOne(queryUserWrapper);
if(null != user){
Ugroup ugroup = ugroupService.getById(user.getGroupId());
userRoles.add(ugroup.getName());
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("idx_group_id",ugroup.getId());
List groupCategories = groupCategoryService.list(queryWrapper);
if (groupCategories.size() > 0){
for (GroupCategory groupCategory : groupCategories){
categoryIds.add(groupCategory.getCategoryId().toString());
}
}
}else{
throw new AuthorizationException();
}
//为当前用户设置角色和权限
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(userRoles);
authorizationInfo.addStringPermissions(categoryIds);
return authorizationInfo;
}
过滤器中重写isAccessAllowed
public class JWTFilter extends AuthorizationFilter {
...
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
logger.info("-----isAccessAllowed----");
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
response401(response);
}
}
String id = request.getParameter("id");
Subject subject = getSubject(request,response);
boolean flag = subject.isPermitted(id);
return flag;
}
...
}
需求:对属于不同用户组的用户授权访问不同类型的文章
实现思路:
doGetAuthorizationInfo中根据token获取到用户信息,从数据库中联合查询到可以访问的文章类型id,添加到用户权限中,isAccessAllowed中获取用户访问链接中的参数,调用
subject.isPermitted(id);
判断是否拥有该权限。
注意点:subject.isPermitted(id)调用前一定要先调用subject.login(token);方法,不然在isPermitted时不会进行授权
下面贴上全部代码
CustomRealm.java
package com.zyc.shiro;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zyc.constant.CommonConstant;
import com.zyc.dao.GroupCategoryMapper;
import com.zyc.dao.UgroupMapper;
import com.zyc.entity.GroupCategory;
import com.zyc.entity.Ugroup;
import com.zyc.entity.User;
import com.zyc.redis.JwtRedisDAO;
import com.zyc.service.GroupCategoryService;
import com.zyc.service.UgroupService;
import com.zyc.service.UserService;
import com.zyc.util.JWTUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
/**
* 鉴权
*/
public class CustomRealm extends AuthorizingRealm {
static Logger logger = LoggerFactory.getLogger(CustomRealm.class);
@Autowired
private UgroupService ugroupService;
@Autowired
private GroupCategoryService groupCategoryService;
@Autowired
private UserService userService;
@Autowired
private JwtRedisDAO jwtRedisDAO;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 获取权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("开始鉴权------------doGetAuthorizationInfo");
User user = (User) principalCollection.getPrimaryPrincipal();
List userRoles = new ArrayList();
Set categoryIds = new HashSet<>();
QueryWrapper queryUserWrapper = new QueryWrapper();
queryUserWrapper.eq("uuid",user.getUuid());
user = userService.getOne(queryUserWrapper);
if(null != user){
Ugroup ugroup = ugroupService.getById(user.getGroupId());
userRoles.add(ugroup.getName());
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("idx_group_id",ugroup.getId());
List groupCategories = groupCategoryService.list(queryWrapper);
if (groupCategories.size() > 0){
for (GroupCategory groupCategory : groupCategories){
categoryIds.add(groupCategory.getCategoryId().toString());
}
}
}else{
throw new AuthorizationException();
}
//为当前用户设置角色和权限
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(userRoles);
authorizationInfo.addStringPermissions(categoryIds);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
logger.info("CustomRealm------------------doGetAuthenticationInfo");
Map claims = JWTUtils.getClaims(CommonConstant.JWT_SECRET, (String) authenticationToken.getPrincipal());
if (claims == null) {
//没找到帐号
throw new UnknownAccountException();
}
String uuid = (String) claims.get("uuid");
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("uuid",uuid);
User user = userService.getOne(queryWrapper);
//logger.info(user.toString());
if(null == user){
throw new UnknownAccountException();
}
String token = jwtRedisDAO.get(CommonConstant.ADMIN_JWT_PREFIX + uuid);
//logger.info("token {}", authenticationToken.getPrincipal());
if (token == null || !token.equals(authenticationToken.getPrincipal())) {
throw new AuthorizationException();
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,可以自定义实现
return new SimpleAuthenticationInfo(
//用户信息
user,
authenticationToken.getPrincipal(),
//realm name
getName()
);
}
}
JWTFilter.java
package com.zyc.shiro;
import com.alibaba.fastjson.JSON;
import com.zyc.exception.enums.ErrorEnums;
import com.zyc.vo.RestResult;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class JWTFilter extends AuthorizationFilter {
private static Logger logger = LoggerFactory.getLogger(JWTFilter.class);
/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
//@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
logger.info("-----isLoginAttempt----");
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return authorization != null;
}
/**
* 实现用户登录
*/
//@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
logger.info("-----executeLogin----");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 该注释并非现在方法的注释,是以前版本的,不要受影响,写在这里只是提醒还有其他写法
*
* 这里我们详细说明下为什么最终返回的都是true,即允许访问
* 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
logger.info("-----isAccessAllowed----");
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
response401(response);
}
}
String id = request.getParameter("id");
Subject subject = getSubject(request,response);
boolean flag = subject.isPermitted(id);
return flag;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
logger.info("-----preHandle----");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", httpServletRequest.getMethod());
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return true;
}
return super.preHandle(request, response);
}
/**
* onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回 true 表示需要继续处理;如果返回 false 表示该拦截器实例已经处理了,将直接返回即可。
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) {
logger.info("当 isAccessAllowed 返回 false 的时候,才会执行 method onAccessDenied ");
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
RestResult result = new RestResult();
result.setCode(ErrorEnums.PERMISSION_DENIED.getCode());
result.setMsg(ErrorEnums.PERMISSION_DENIED.getMsg());
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.print(JSON.toJSONString(result));
out.flush();
out.close();
} catch (IOException e) {
logger.error(e.getMessage());
}
// 返回 false 表示已经处理,例如页面跳转啥的,表示不在走以下的拦截器了(如果还有配置的话)
return false;
}
/**
* 非法请求返回code401
*/
private void response401(ServletResponse resp) {
logger.info("-----response401----");
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
RestResult result = new RestResult();
result.setCode(ErrorEnums.TOKEN_MISS.getCode());
result.setMsg(ErrorEnums.TOKEN_MISS.getMsg());
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.print(JSON.toJSONString(result));
out.flush();
out.close();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
JWTToken.java
package com.zyc.shiro;
import org.apache.shiro.authc.AuthenticationToken;
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return getPrincipal();
}
}
ShiroConfigurer.java
package com.zyc;
import com.zyc.shiro.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.authz.permission.PermissionResolver;
import org.apache.shiro.authz.permission.RolePermissionResolver;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import java.util.*;
@Configuration
public class ShiroConfigurer {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfigurer.class);
/**
* Shiro的Web过滤器Factory 命名:shiroFilter
*
* @param securityManager
* @return
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
logger.info("注入Shiro的Web过滤器-->shiroFilter {}", ShiroFilterFactoryBean.class);
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
// shiroFilterFactoryBean.setLoginUrl("/login")
// 登录成功后要跳转的连接,逻辑也可以自定义,例如返回上次请求的页面
// shiroFilterFactoryBean.setSuccessUrl("/index")
// 用户访问未对其授权的资源时,所显示的连接
// shiroFilterFactoryBean.setUnauthorizedUrl("/pages/403")
/* 定义shiro过滤器,例如实现自定义的FormAuthenticationFilter,需要继承FormAuthenticationFilter
**本例中暂不自定义实现,在下一节实现验证码的例子中体现
*/
/*定义shiro过滤链 Map结构
* Map中key(xml中是指value值)的第一个'/'代表的路径是相对于HttpServletRequest.getContextPath()的值来的
* anon:它对应的过滤器里面是空的,什么都没做,这里.do和.jsp后面的*表示参数,比方说login.jsp?main这种
* authc:该过滤器下的页面必须验证后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
*/
// 添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap();
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map filterChainDefinitionMap = new LinkedHashMap<>();
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
// :这是一个坑呢,一不小心代码就不好使了
//
// filterChainDefinitionMap.put("/webui/**", "anon")
// filterChainDefinitionMap.put("/webjars/**", "anon")
//filterChainDefinitionMap.put("/sys/login", "anon");
//filterChainDefinitionMap.put("/sys/logout", "anon");
//filterChainDefinitionMap.put("/token/callback", "anon");
//filterChainDefinitionMap.put("/dist", "anon");
//filterChainDefinitionMap.put("/download/excel", "anon");
//登陆相关api不需要被过滤器拦截
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 所有请求通过JWT Filter
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public JWTFilter jwtFilter() {
return new JWTFilter();
}
/**
* Shiro Realm 继承自AuthorizingRealm的自定义Realm,即指定Shiro验证用户登录的类为自定义的
*
* @param
* @return managerRealm
*/
@Bean
public CustomRealm userRealm() {
CustomRealm userRealm = new CustomRealm();
// 告诉realm,使用credentialsMatcher加密算法类来验证密文
// userRealm.setCredentialsMatcher(hashedCredentialsMatcher())
userRealm.setCachingEnabled(false);
//自定义权限解析器
return userRealm;
}
/**
* 不指定名字的话,自动创建一个方法名第一个字母小写的bean
*
* @return
* @Bean(name = "securityManager")
*/
@Bean
public SecurityManager securityManager() {
logger.info("注入Shiro的Web过滤器-->securityManager {}", ShiroFilterFactoryBean.class);
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
// 关闭自带session
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码;
* )
* 可以扩展凭证匹配器,实现 输入密码错误次数后锁定等功能,下一次
*
* @return
*/
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("SHA-512");
//散列的次数,比如散列两次,相当于 md5(md5(""))
hashedCredentialsMatcher.setHashIterations(2);
//storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/**
* Shiro生命周期处理器
*
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
advisorAutoProxyCreator.setUsePrefix(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
pom.xml
org.springframework.boot
spring-boot-starter-aop
io.jsonwebtoken
jjwt
0.7.0
org.apache.shiro
shiro-spring
1.3.2