本文将带你从0开始搭建一个Springboot+shiro动态权限控制
首先你得知道什么是Shiro?
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解API,你可以快速、轻松地获取任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
官网:[点我进入]
开发环境:
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!-- springboot --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> </parent>
1.编写LoginController
@PostMapping("/login")
@ApiOperation("登录系统")
@Log(module = "登录系统", description = "登陆系统")
public ResultResponse login(@RequestBody LoginInoutDTO inoutDTO) {
if (StringUtils.isBlank(inoutDTO.getUsername()) || StringUtils.isBlank(inoutDTO.getPassword())) {
return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "参数不全", null);
}
//当前登录的用户
Subject currentUser = SecurityUtils.getSubject();
// 如果这个用户没有登录,进行登录功能
if (!currentUser.isAuthenticated()) {
try {
// 验证身份和登陆
UsernamePasswordToken token = new UsernamePasswordToken(inoutDTO.getUsername(), inoutDTO.getPassword());
currentUser.login(token);
permissionsConfig.updatePermission();
} catch (UnknownAccountException e) {
return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "此账号不存在!", null);
} catch (IncorrectCredentialsException e) {
return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "用户名或者密码错误,请重试!", null);
} catch (LockedAccountException e) {
return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "该账号已被锁定,请联系管理员!", null);
} catch (AuthenticationException e) {
return new ResultResponse(false, ErrorDetail.RC_0401001.getIndex(), "未知错误,请联系管理员!", null);
}
}
return new ResultResponse(true, ErrorDetail.RC_0000000.getIndex(), "登录成功", userInfo);
}
}
2.自定义Realm
/**
* Created by Eagga_Lo on 2019/11/26 14:19
*/
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Autowired
RoleService roleService;
@Autowired
MenuService menuService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("--------------开始授权操作--------------");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principalCollection.getPrimaryPrincipal();
log.info("user:{}", user.toString());
Set<String> rolesSet = new HashSet<>();
Set<String> permsSet = new HashSet<>();
try {
List<Role> roleList = roleService.selectRoleByUserId(user.getById());
for (Role role : roleList) {
rolesSet.add(role.getRoleId());
List<XtZzjgMenu> menuList = menuService.selectMenuByRoleId(role.getRoleId());
for (XtZzjgMenu menu : menuList) {
permsSet.add(menu.getResource());
}
}
log.info("--------------当前用户的角色:{}--------------", rolesSet);
log.info("--------------当前角色的权限:{}--------------", permsSet);
//将查到的权限和角色分别传入authorizationInfo中
authorizationInfo.setStringPermissions(permsSet);
authorizationInfo.setRoles(rolesSet);
log.info("--------------赋予角色和权限成功---------------");
} catch (Exception e) {
e.printStackTrace();
log.info("--------------赋予角色和权限失败{}--------------", e.getMessage());
}
return authorizationInfo;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("---------------开始认证操作---------------");
UsernamePasswordToken tokenInfo = (UsernamePasswordToken) authenticationToken;
// 获取用户输入的账号
String username = tokenInfo.getUsername();
// 获取用户输入的密码
String password = String.valueOf(tokenInfo.getPassword());
//数据库中的user
User user = userService.selectByPrimaryKey(username);
if (user == null) {
return null;
}
// 判断是否为冻结状态
if ("Y".equals(user.getSfsc())) {
throw new LockedAccountException();
}
/*
* 进行验证 -> 注:shiro会自动验证密码
* 参数1:principal -> 放对象就可以在页面任意地方拿到该对象里面的值
* 参数2:hashedCredentials -> 密码
* 参数3:credentialsSalt -> 设置盐值
* 参数4:realmName -> 自定义的Realm
*/
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
log.info("--------------- 认证完毕! ---------------");
return authenticationInfo;
}
}
3.前后端分离需要过滤OPTIONS请求,否则可能出现302错误。
@Slf4j
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
public MyFormAuthenticationFilter() {
super();
}
/**
* 过滤OPTIONS请求 不配置可能会报302错误
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//过滤OPTIONS请求
String method = ((HttpServletRequest) request).getMethod().toUpperCase();
if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
return true;
}
} else {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
resp.setStatus(HttpStatus.OK.value());
return true;
}
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
//前端Ajax请求时requestHeader里面带一些参数,用于判断是否是前端的请求
String test = req.getHeader("test");
if (test != null || req.getHeader("wkcheck") != null) {
//前端Ajax请求,则不会重定向
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setContentType("application/json; charset=utf-8");
resp.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
JSONObject result = new JSONObject();
result.put("message", "登录失效");
result.put("resultCode", 1000);
out.println(result);
out.flush();
out.close();
} else {
saveRequestAndRedirectToLogin(request, response);
}
return false;
}
}
}
4.自定义过滤器继承PermissionsAuthorizationFilter解决由于权限不足直接报302错误。
/**
* 自定义url过滤权限
*/
@Slf4j
public class MyPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
/**
* 解决权限不足302问题
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestUrl = httpRequest.getServletPath();
log.info("请求的url: " + requestUrl);
// 检查是否拥有访问权限
Subject subject = this.getSubject(request, response);
if (subject.getPrincipal() == null) {
this.saveRequestAndRedirectToLogin(request, response);
} else {
// 转换成http的请求和响应
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 获取请求头的值
String header = req.getHeader("X-Requested-With");
// ajax 的请求头里有X-Requested-With: XMLHttpRequest 正常请求没有
if ("XMLHttpRequest".equals(header)) {
resp.setContentType("text/json,charset=UTF-8");
ResultResponse resultResponse = new ResultResponse(false, "302", "权限不足", null);
log.info("----------权限不足---------");
resp.getWriter().print(resultResponse);
} else {
//正常请求
String unauthorizedUrl = this.getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(401);
}
}
}
return false;
}
}
5.自定义过滤器继承AuthorizationFilter,原生过滤器必须满足所有角色都有才允许访问。
/**
* 原生的角色过滤器RolesAuthorizationFilter 默认是必须同时满足roles[admin,guest]才有权限
*/
@Slf4j
public class MyRolesAuthorizationFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) throws Exception {
Subject subject = getSubject(req, resp);
String[] rolesArray = (String[]) mappedValue;
// 没有角色限制,有权限访问
if (rolesArray == null || rolesArray.length == 0) {
return true;
}
for (String s : rolesArray) {
//若当前用户是rolesArray中的任何一个,则有权限访问
if (subject.hasRole(s)) {
return true;
}
}
return false;
}
}
6.自定义Session管理器 继承DefaultWebSessionManager
注意:此处没有采用Redis作为session缓存,让前端加请求头进行session传递。请求头必须与“AUTHORIZATION ”的值一致。
/**
* Created by Eagga_Lo on 2019/11/26 14:16
*/
@Slf4j
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
/**
* 采用自定义SessionManager的方式自己来管理sessionid的获取
* 这样前端需要做的就是每次请求,要把后端传给它的sessionid即token
* 放到请求头里key为Authorization,value为后台传过来的token
* 然后用自定义的SessionManager获取就可以了
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//前端ajax的headers中必须传入Authorization的值
String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(sessionId)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
7.自定义ShiroConfig配置类
/**
* Created by Eagga_Lo on 2019/11/26 14:14
*/
@Configuration
public class ShiroConfig {
@Autowired
MenuMapper menuMapper;
@Autowired
RoleMapper roleMapper;
@Autowired
PermissionsConfig permissionsConfig;
/**
* 请求拦截
*
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 自定义过滤器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("MyAuthc", new MyFormAuthenticationFilter());
filterMap.put("MyPerms", new MyPermissionsAuthorizationFilter());
filterMap.put("MyRoles", new MyRolesAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 登录的路径: 如果你没有登录则会跳到这个页面中 - 如果没有设置值则会默认跳转到工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/unLogin");
// 设置没有权限时跳转的url
shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
// 加载权限
shiroFilterFactoryBean.setFilterChainDefinitionMap(permissionsConfig.loadFilterChainDefinitions());
return shiroFilterFactoryBean;
}
/**
* @Description: SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(ShiroRealm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 自定义认证
*
* @return MyShiroRealm
* @Title: myShiroRealm
* @Description: ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,负责用户的认证和权限的处理
*/
@Bean
public ShiroRealm ShiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* 密码凭证匹配器,作为自定义认证的基础 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 )
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密的次数
hashedCredentialsMatcher.setHashIterations(1024);
//此处的设置,true加密用的hex编码,false用的base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
/**
* 自定义sessionManager,用户的唯一标识,即Token或Authorization的认证
*/
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setGlobalSessionTimeout(-1L);
return mySessionManager;
}
}
8.自定义实现动态加载权限
最后一步也是最重要的一步,动态加载FilterChainDefinitionMap,也就是shiroFilterFactoryBean.setFilterChainDefinitionMap(permissionsConfig.loadFilterChainDefinitions())这一步。
之所以需要动态刷新权限,是因为当我们新增了用户,新增角色,然后重新分配权限,用这个账户直接登录时,这个用户和角色并没有被初始化到FilterChainDefinitionMap里面,大家debug这一步的时候想必也知道只有在开启服务的时候会调用一次,所以这时候就需要动态加载FilterChainDefinitionMap,在增删改查涉及到角色,权限等地方调用进行刷新权限。刷新之后FilterChainDefinitionMap里就会加载出我们动态查询出的所有权限了。
/**
* Created by Eagga_Lo on 2019/12/23 16:41
*/
@Service
@Slf4j
public class PermissionsConfig {
@Autowired
MenuMapper menuMapper;
@Autowired
RoleMapper roleMapper;
public Map<String, String> loadFilterChainDefinitions() {
// 权限控制
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断
//authc 必须认证通过才可以访问;
//anon 可以匿名访问
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
// 登录
filterChainDefinitionMap.put("/login", "anon");
// 退出登录
filterChainDefinitionMap.put("/logout", "anon");
// 未登录
filterChainDefinitionMap.put("/unLogin", "anon");
// 未授权
filterChainDefinitionMap.put("/unauth", "anon");
// 自定义权限
List<Menu> permissionList = menuMapper.selectAll();
if (!CollectionUtils.isEmpty(permissionList)) {
permissionList.forEach(e -> {
if (StringUtils.isNotBlank(e.getUrl())) {
// 根据url查询相关联的角色名,拼接自定义的角色权限
List<Role> roleList = roleMapper.selectRoleByMenuId(e.getMenuId());
StringJoiner MyRoles = new StringJoiner(",", "MyRoles[", "]");
if (!CollectionUtils.isEmpty(roleList)) {
roleList.forEach(f -> {
MyRoles.add(f.getRoleDes());
});
}
filterChainDefinitionMap.put(e.getUrl(), "MyAuthc," + MyRoles.toString() + ",MyPerms[" + e.getResource() + "]");
}
});
}
filterChainDefinitionMap.put("/**", "MyAuthc");
return filterChainDefinitionMap;
}
/**
* 更新权限,解决需要重启tomcat才能生效权限的问题
*/
public void updatePermission() {
log.info("-----------------正在更新权限...--------------");
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
ServletContext servletContext = request.getSession().getServletContext();
//注意bean的名字不要写错了 是自己的shiroFilter名称
AbstractShiroFilter shiroFilter = (AbstractShiroFilter) WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext).getBean("shiroFilter");
synchronized (shiroFilter) {
// 获取过滤管理器
PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
// 清空初始权限配置
manager.getFilterChains().clear();
// 重新获取资源
Map<String, String> chains = loadFilterChainDefinitions();
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue().trim().replace(" ", "");
manager.createChain(url, chainDefinition);
}
log.info("-----------------更新权限成功!!--------------");
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}