情景:这是个后台系统之前是MVC项目这次也是要改造为boot,这里涉及到了shiro框架的集成,幸运的是我之前有独自用boot集成shiro过的小项目,所以差不多这里就是依样画葫芦,难度不是很大,但是做完后,交给同事,同事发现请求相关接口时,即使没有登录也可以访问接口,那么问题来了,这是为什么呢?
解决:首先来说说怎么集成shiro;
1.pom依赖,版本自行选择
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
</dependency>
2.自定义的密码验证器,这个类的作用是自己设置密码验证策略
package com.rt.platform.admin.common.shiro.credentials;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
private static ConcurrentHashMap<String, Entry> passwordRetryCache = new ConcurrentHashMap<String, Entry>();
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String)token.getPrincipal();
//retry count + 1
Entry entry = passwordRetryCache.get(username);
if(entry == null) {
entry = new Entry(TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis()), new AtomicInteger(0));
passwordRetryCache.put(username, entry);
}
//timeunit
if(TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis()) - entry.lastTime > 10){//TODO 改成配置项
passwordRetryCache.remove(username);
}else if(entry.count.incrementAndGet() > 5) {
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if(matches) {
//clear retry count
passwordRetryCache.remove(username);
}
return matches;
}
private static class Entry{
private Entry(long lastTime, AtomicInteger count){
this.count = count;
this.lastTime = lastTime;
}
private AtomicInteger count;
private long lastTime;
}
}
3.自定义表单提交拦截器,作用是拦截表单提交请求,主要就是登录的请求;
package com.rt.platform.admin.common.shiro.filter;
import com.rt.platform.admin.common.shiro.user.dto.AdminUserDto;
import com.rt.platform.admin.common.shiro.user.service.IAdminUserService;
import com.rt.platform.admin.common.shiro.user.vo.AdminUserVo;
import com.rt.platform.infosys.base.common.utils.NetworkUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
public class AdminFormAuthenticationFilter extends FormAuthenticationFilter{
private Logger LOG = LoggerFactory.getLogger(AdminFormAuthenticationFilter.class);
@Resource
private IAdminUserService iAdminUserService;
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
AdminUserDto user = iAdminUserService.getAdminUserByLoginName((String)token.getPrincipal());
LOG.info("login success!login user id is:{},user name is:{}",user.getId(),user.getLoginName());
AdminUserVo vo = new AdminUserVo();
vo.setId(user.getId());
vo.setLoginTime(new Date());
vo.setLoginIp(NetworkUtil.getClientIP((HttpServletRequest)request));
iAdminUserService.updateAdminUser(vo, null, false);
HttpServletRequest httpRequest = (HttpServletRequest) request;
httpRequest.getRequestDispatcher(this.getSuccessUrl()).forward(httpRequest, response);
return true;
}
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
ServletResponse response) {
return super.onLoginFailure(token, e, request, response);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
return super.onAccessDenied(request, response);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
return super.onAccessDenied(request, response, mappedValue);
}
}
4.自定义userFilter,作用是类似记住密码的功能;
package com.rt.platform.admin.common.shiro.filter;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class ShiroUserFilter extends UserFilter{
public static final Logger LOG = LoggerFactory.getLogger(ShiroUserFilter.class);
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
LOG.info("====================shiro user filter!");
//自定义相关操作
return true;
}
}
5.自定义shiro数据源,作用是对一些shiro相关权限的配置,例如,角色的permission等
package com.rt.platform.admin.common.shiro;
import com.rt.platform.admin.common.constants.AdminGlobalConstants;
import com.rt.platform.admin.common.shiro.user.dto.AdminPermissionDto;
import com.rt.platform.admin.common.shiro.user.dto.AdminUserDto;
import com.rt.platform.admin.common.shiro.user.dto.AdminUserRoleDto;
import com.rt.platform.admin.common.shiro.user.service.IAdminPermissionService;
import com.rt.platform.admin.common.shiro.user.service.IAdminUserService;
import org.apache.shiro.authc.*;
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.apache.shiro.util.ByteSource;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
public class AdminRealm extends AuthorizingRealm {
@Resource
private IAdminUserService iAdminUserService;
@Resource
private IAdminPermissionService iAdminPermissionService;
@Override
public String getName() {
return "AuthorizingRealm";
}
// 支持UsernamePasswordToken
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String loginName = (String) principals.getPrimaryPrincipal();
AdminUserDto user = iAdminUserService.getAdminUserByLoginName(loginName);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 根据用户查询对应的角色
List<AdminUserRoleDto> roleList = iAdminUserService.getAdminUserRoleByUserId(user.getId());
// 组装角色集合
Set<String> roles = new HashSet<String>();
List<Long> roleIds = new ArrayList<Long>();
if (roleList != null && roleList.size() > 0) {
for (AdminUserRoleDto userRole : roleList) {
if (userRole == null) {
continue;
}
roleIds.add(userRole.getRoleId());
roles.add(userRole.getRoleId().toString());
}
}
// 设置角色
authorizationInfo.setRoles(roles);
// 根据角色查询权限
List<AdminPermissionDto> permissionList = iAdminPermissionService.getPermissionByRoleId(roleIds);
Set<String> permissions = permissionList.stream().filter(dto -> dto != null)
.map(AdminPermissionDto::getPermissionKey).collect(Collectors.toSet());
// 设置权限
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
if(Objects.isNull(token.getPrincipal())){
return null;
}
String loginName = token.getPrincipal().toString();
AdminUserDto user = iAdminUserService.getAdminUserByLoginName(loginName);
if (user == null) {
throw new UnknownAccountException();// 没找到帐号
}
if (AdminGlobalConstants.INVALID == user.getIsValid()) {
throw new LockedAccountException(); // 帐号锁定
}
// 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getLoginName(), // 用户名
user.getPassword(), // 密码
ByteSource.Util.bytes(user.getLoginName() + AdminGlobalConstants.SALT), // salt=loginName+salt
getName() // realm name
);
return authenticationInfo;
}
}
6.重点来是接下去的这个类,用boot配置bean的方式配置原来MVC项目中xml的相关配置,如下类
package com.rt.platform.admin.common.shiro;
import com.rt.platform.admin.common.shiro.credentials.RetryLimitHashedCredentialsMatcher;
import com.rt.platform.admin.common.shiro.filter.AdminFormAuthenticationFilter;
import com.rt.platform.admin.common.shiro.filter.ShiroUserFilter;
import com.rt.platform.admin.common.shiro.filter.SysUserFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
@Configuration
public class ShiroConfiguration {
//shiro数据源的bean
@Bean
public AdminRealm adminRealm(RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher) {
AdminRealm adminRealm = new AdminRealm();
adminRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher);
adminRealm.setCachingEnabled(false);
return adminRealm;
}
//安全管理器bean
@Bean
public SecurityManager securityManager(AdminRealm adminRealm,CookieRememberMeManager cookieRememberMeManager,
ServletContainerSessionManager sessionManager
) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(adminRealm);
securityManager.setRememberMeManager(cookieRememberMeManager);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public MethodInvokingFactoryBean methodInvokingFactoryBean(SecurityManager securityManager) {
MethodInvokingFactoryBean methodInvokingFactoryBean=new MethodInvokingFactoryBean();
Object[] objects=new Object[]{securityManager};
methodInvokingFactoryBean.setArguments(objects);
methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
return methodInvokingFactoryBean;
}
//主要关注这个别的基本可以忽略
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, AdminFormAuthenticationFilter adminFormAuthenticationFilter,
ShiroUserFilter shiroUserFilter,SysUserFilter sysUserFilter) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
LinkedHashMap<String, Filter> map2 = new LinkedHashMap<String, Filter>();
map2.put("adminFormAuthenticationFilter",adminFormAuthenticationFilter);
map2.put("shiroUserFilter",shiroUserFilter);
map2.put("sysUserFilter",sysUserFilter);
shiroFilterFactoryBean.setFilters(map2);
//登录
shiroFilterFactoryBean.setLoginUrl("/login.html");
shiroFilterFactoryBean.setSuccessUrl("/index.html");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/404.html");
shiroFilterFactoryBean.setSecurityManager(securityManager);
//对所有用户认证
map.put("/admin/login", "adminFormAuthenticationFilter");
//登出
map.put("/logout", "logout");
map.put(" /**/*.html", "anon");
map.put("/**/*.js", "anon");
map.put("/webjar/**", "anon");
map.put("/**", "sysUserFilter,shiroUserFilter,authc");//如果将authc改为adminFormAuthenticationFilter会报请求方式错误,如果不加authc会出现,任何请求都可以匿名访问
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//开启注解配置
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public SimpleCookie simpleCookie() {
SimpleCookie simpleCookie=new SimpleCookie("rememberMe");
simpleCookie.setHttpOnly(true);
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
@Bean
public ServletContainerSessionManager sessionManager() {
return new ServletContainerSessionManager();
}
@Bean
public CookieRememberMeManager cookieRememberMeManager(SimpleCookie simpleCookie) {
CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager();
cookieRememberMeManager.setCipherKey("#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}".getBytes());
cookieRememberMeManager.setCookie(simpleCookie);
return cookieRememberMeManager;
}
//表单提交登录验证的bean
@Bean
public AdminFormAuthenticationFilter adminFormAuthenticationFilter(){
AdminFormAuthenticationFilter adminFormAuthenticationFilter = new AdminFormAuthenticationFilter();
adminFormAuthenticationFilter.setUsernameParam("username");
adminFormAuthenticationFilter.setPasswordParam("password");
adminFormAuthenticationFilter.setRememberMeParam("rememberMe");
adminFormAuthenticationFilter.setLoginUrl("/admin/login");
adminFormAuthenticationFilter.setSuccessUrl("/admin/loginSuccess");
return adminFormAuthenticationFilter;
}
@Bean
public ShiroUserFilter shiroUserFilter(){
return new ShiroUserFilter();
}
@Bean
public SysUserFilter sysUserFilter(){
return new SysUserFilter();
}
//密码策略的bean
@Bean
public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher(){
RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher();
retryLimitHashedCredentialsMatcher.setHashAlgorithmName("md5");
retryLimitHashedCredentialsMatcher.setHashIterations(2);
retryLimitHashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return retryLimitHashedCredentialsMatcher;
}
}
相关集成基本到这里结束了,注意点有
1.ShiroFilterFactoryBean 的配置,如果不配置authc这个默认的过滤器的话,就不能拦截所有需要权限的请求,因为我后台配置的登录接口,是post所以假如我将authc改为自定义的adminFormAuthenticationFilter,然后我用get方式去请求其他接口时,会报请求方式错误。
2.请求规则的添加,注意规则是从上到下匹配的,如果之前匹配到了的规则就不会走下面的匹配规则,意味着走拦截器的顺序就会不一样。