一、概述
Shiro 是一个功能强大且易于使用的轻量级Java安全框架,包括身份验证、授权、加密及会话管理,使用Shiro易于理解的API,可以轻松地保护任何应用程序。
二、Shiro主要组成
1.首先主要包括三大实体:Subject、Realm、和SecurityManager
Subject即当前用户,我们可以通过Subject自带的 。SecurityUtils.getSubject()方法获取当前对象,并通过当前用户拿到shiro的session,进行后续的认证授权等。
SecurityManager即安全管理器,可以视为拦截器,拦截请求,并转至shiro中处理。
Realm,可以有1个或多个Realm,即安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;一般由用户自定义实现。
三、Springboot如何使用Shiro实现登录认证
1.添加依赖
org.apache.shiro
shiro-spring
1.4.0
2.定义Shiro配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import com.suntree.entity.SysUser;
import com.suntree.service.UserService;
import com.suntree.utils.YAMLUtils;
import com.suntree.entity.Power;
import com.suntree.utils.CommonUtil;
import com.suntree.entity.Role;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
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.subject.PrincipalCollection;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
@Configuration
public class ShiroConfig {
@Autowired
private YAMLUtils yaml;
private static Log logger = LogFactory.getLog(ShiroConfig.class);
@Bean
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) throws Exception {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map filters = new LinkedHashMap();
shiroFilterFactoryBean.setFilters(filters);
//权限控制map
HashMap filterMap = new LinkedHashMap<>();
filterMap.put("/api/city/page", "anon");
filterMap.put("/api/topic/", "anon");
filterMap.put("/api/**", "authc");
// filterMap.put(yaml.logout_url, "logout");
// 登录 url
shiroFilterFactoryBean.setLoginUrl(yaml.login_url);
// 未授权 url
shiroFilterFactoryBean.setUnauthorizedUrl(yaml.unauthorized_url);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(@Qualifier("myShiroRealm") MyShiroRealm myShiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm);
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
*
* @return
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(credentialsMatcher());
myShiroRealm.setCacheManager(cacheManager());//設置緩存
return myShiroRealm;
}
/*
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}*/
/**
* cacheManager 缓存 redis实现
*
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* 配置shiro redisManager
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(yaml.redis_host);
redisManager.setPort(yaml.redis_port);
redisManager.setExpire(yaml.redis_cache);// 配置过期时间
redisManager.setTimeout(0);
redisManager.setPassword(yaml.redis_passwd);
logger.info("配置redis连接设置##########" + yaml.redis_host + ":::" + yaml.redis_port);
return redisManager;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// Collection listeners = new ArrayList();
// listeners.add(new BDSessionListener());
// sessionManager.setSessionListeners(listeners);
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO sessionDAO() {
logger.info("是否使用redis缓存");
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
logger.info("设置redisSessionDAO");
return redisSessionDAO;
}
/**
* 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持; Controller才能使用@RequiresPermissions
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
// log.info("authorizationAttributeSourceAdvisor()");
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* AOP式方法级权限检查
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator=new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
/**
* @return 密码匹配器
*/
@Bean
public CredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
return hashedCredentialsMatcher;
}
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
@Lazy
private UserService userService;
//private final Log log = LogFactory.getLog(MyShiroRealm.class);
/**
* 用户验证
* @param token 账户数据
* @return
* @throws AuthenticationException 根据账户数据查询账户。根据账户状态抛出对应的异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号
String accNum = (String) token.getPrincipal();
//这里需注意。看别人的教程有人是这样写的String password = (String) token.getCredentials();
//项目运行的时候报错,发现密码不正确。后来进源码查看发现将密码注入后。Shiro会进行转义将字符串转换成字符数组。
//源码:this(username, password != null ? password.toCharArray() : null, false, null);
//不晓得是否是因为版本的原因,建议使用的时候下载源码进行查看
// String password = new String((char[]) token.getCredentials());
//通过username从数据库中查找 User对象,如果找到,没找到.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
SysUser user = userService.getUserByAcc(accNum);
// SysUser sysUser = userService.getUserByAcc(accNum);
// sysUser.setPasswd("");
if(user==null){
throw new UnknownAccountException();
} else if(yaml.is_lock.equals(user.getIsLock())) {
throw new LockedAccountException();
}else {
SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(user,user.getPasswd(),this.getName());
return authorizationInfo;
}
}
/**
* 配置权限 注入权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
System.out.println("--------权限配置-------");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
SysUser user = (SysUser) principals.getPrimaryPrincipal();
try {
user = userService.getUserDetailInfoByUserId(user.getUserID());
//注入角色(查询所有的角色注入控制器)
List roleList = user.getRoleList();
for (Role role: roleList){
authorizationInfo.addRole(role.getRoleName());
//注入角色所有权限(查询用户所有的权限注入控制器)
if(CommonUtil.check(role.getPowerList())) {
for (Power permission : role.getPowerList()) {
authorizationInfo.addStringPermission(permission.getUrl());
}
}
}
}catch (Exception e){
e.printStackTrace();
logger.error(e.getMessage());
}
return authorizationInfo;
}
}
}
此处进行登录验证时使用了MD5加密,即数据库中存储的密码应是MD5加密后的密文密码,而不是明文密码。
3.在Controller层编写登录接口
import java.util.Date;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.suntree.service.UserService;
import com.suntree.utils.CommonUtil;
import com.suntree.utils.DateUtil;
import com.suntree.utils.YAMLUtils;
import com.suntree.commons.ApiResult;
import com.suntree.entity.SysUser;
import com.suntree.entity.UserToken;
import com.suntree.mapper.UserTokenMapper;
@RestController
public class LoginController {
@Autowired
private UserService userService;
@Autowired
private UserTokenMapper userTokenMapper;
@Autowired
private YAMLUtils yaml;
/**
* 登录
* @param user
* @return
*/
@PostMapping("/login")
public ApiResult login(@RequestBody SysUser user) {
ApiResult result = new ApiResult();
UserToken userToken=userTokenMapper.getTokenByToken(user.getToken());
String kaptcha=user.getKaptcha();
if(!kaptcha.equalsIgnoreCase(userToken.getKaptcha())) {
return result.failed("验证码错误");
}
if(DateUtil.checkBegTmAndEndTm(user.getExpireTm(),DateUtil.dateToYMDHMS(new Date()))) {
return result.failed("验证码已过期");
}
Subject subject = SecurityUtils.getSubject();
subject.getSession().setTimeout(yaml.session_timeout);
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getAccNum(), user.getPasswd());
try {
subject.login(usernamePasswordToken);
} catch (UnknownAccountException uae) {
return result.failed("账号或密码错误");
} catch (IncorrectCredentialsException ice) {
return result.failed("账号或密码错误");
} catch (LockedAccountException lae) {
return result.failed("账号被冻结");
} catch (RuntimeException re) {
return result.failed(re.getMessage());
}
SysUser loginUser=CommonUtil.getLoginUser();
SysUser data = userService.getUserDetailInfoByUserId(loginUser.getUserID());
return result.success(data, "登录成功");
}
/**
* 退出登录
* @return
*/
@GetMapping("/logout")
public ApiResult logout() {
ApiResult result = new ApiResult();
Subject subject = SecurityUtils.getSubject();
subject.logout();
subject.getSession().setTimeout(1000);
return result.success("退出登录成功", "退出登录成功");
}
这样就实现了使用Shiro完成登录认证,大家可以自己尝试下。
另外,如果我们想要控制对于接口访问权限,也可以使用Shiro相关注解实现,比如像 @RequestPermissions这样的注解就可以进行控制用户的权限,这里就不做过多说明了。