1.什么是Shiro,为什么用它
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
安全框架常用还有Spring Security 但是Spring Security与Spring依赖太高,而且还复杂
Shiro简单,容易上手
2.如何使用
2.1了解Shiro架构
Shiro架构从外面看主要分为
Subject
用户
SecurityManager
安全管理
Realm
领域 充当Shiro与应用程序安全数据之间的桥梁
这里举个梨子
小王就是Subject,网吧就是SecurityManager安全管理器,去公安局获取数据就是Realm
shiro主要负责的就是认证与鉴权
2.1.1 SecurityManager
安全管理器:即所有与安全有关的操作都会与其交互,管理着subject;可以看出他是shiro的核心,与其他组件进行交互。
由上图可以看出SecurityManager里面有
Authenticator:负责Subject认证,是一个拓展点,可以自定义实现;可以使用认证,策略。
Authorizer:授权器,用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能;
Realm:可以有1个多个Realm,可以认为是安全实体数据源,即用于安全实体的;可以是JDBC实现,由用户提供,所以一般在应用中都需要实现自己的Realm;
SessionManager:管理Session生命周期的组件;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境
CacheManager:缓存控制器,来管理如用户,角色,权限等的缓存的;因为这些数据基本上很少改变,放到缓存
2.2导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
</parent>
<!-- 修改参数 -->
<properties>
<!-- 修改JDK的编译版本为1.8 -->
<java.version>1.8</java.version>
<!-- 修改thymeleaf的版本 -->
<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.0.4</thymeleaf-layout-dialect.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 导入thymeleaf依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- shiro与spring整合依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro与redis整合依赖 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
在这里插入代码片
2.3配置文件
@Configuration
public class ShiroConfig {
/**
* 创建ShiroFilterFactoryBean 过滤器
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
/**
* 设置安全管理器
*/
shiroFilterFactoryBean.setSecurityManager(securityManager);
/**
* Shiro内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon: 无需认证(登录)可以访问
* authc: 必须认证才可以访问
* user: 如果使用rememberMe的功能可以直接访问
* perms: 该资源必须得到资源权限才可以访问
* role: 该资源必须得到角色权限才可以访问
*/
shiroFilterFactoryBean.setLoginUrl("/admin/toLogin");
//设置未授权提示页面
shiroFilterFactoryBean.setUnauthorizedUrl("/admin/toUnauthorized");
//设置路径映射,注意这里要用LinkedHashMap 保证有序
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
//对所有页面进行认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/* @Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}*/
/**
* 创建 DefaultWebSecurityManager
*/
@Bean
public SecurityManager getSecurityManager(MyShiroRealm myShiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm);
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(cacheManager());
return securityManager;
}
/**
* 加密算法
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//采用MD5 进行加密
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密次数
hashedCredentialsMatcher.setHashIterations(1);
return hashedCredentialsMatcher;
}
@Bean
public MyShiroRealm getRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
UserRealm shiroRealm = new UserRealm();
//校验密码用到的算法
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return shiroRealm;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Value("${spring.redis.host}")
String host;
@Value("${spring.redis.port}")
int port;
@Value("${spring.redis.password}")
String passWord;
/**
* redis管理类
*/
public RedisManager redisManager() {
//Redis集群使用RedisClusterManager,单个Redis使用RedisManager
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
//redisManager.setPassword(passWord);
return redisManager;
}
/**
* sessionDao
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDao = new RedisSessionDAO();
sessionDao.setRedisManager(redisManager());
return sessionDao;
}
/**
* session管理
*/
public DefaultWebSessionManager sessionManager() {
CustomSessionManger customSessionManger = new CustomSessionManger();
customSessionManger.setSessionDAO(redisSessionDAO());
//不显示JSESSIONID
customSessionManger.setSessionIdUrlRewritingEnabled(false);
return customSessionManger;
}
/**
*缓存管理区
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager=new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
}
2.4 授权
在shiro中可以使用过滤器的方式配置目标地址的请求权限
2.4.1 基于配置授权
2.4.1.1 默认过滤器
/**
* anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
* <p>
* authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数
* <p>
* roles(角色):例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
* <p>
* perms(权限):例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
* <p>
* rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
* <p>
* port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString
* <p>
* 是你访问的url里的?后面的参数。
* <p>
* authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
* <p>
* ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
* <p>
* user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查
*/
2.4.1.2 匹配规则
1."?":匹配一个字符,如“/login?”将匹配"/login123123123","/login654321",但是不匹配“/login”
2."*":匹配零个或者多个字符,如“/login*”将匹配"/login1","/login",但是不匹配“/login/1”
3."**":匹配多个或者零个路径,如“/login/* *”(markdown 问题 * * 之间应该是没有空格的),将匹配“/login/1”,“/login/2”
2.4.1.3 匹配顺序
按顺序匹配,如果第一个匹配了,就直接放行
2.4.2 基于注解
1.RequiresPermissions
配置到方法上,表明执行此方法必须具有指定的权限
//查询
@RequiresPermissions(value = "user-find")
public String find() {
return "查询用户成功";
}
2.RequiresRoles
配置到方法上,表明执行此方法必须具有指定的角色
@RequiresRoles(value = "root")
public String find() {
return "查询用户成功";
}
2.4自定义会话管理
2.4.1什么是Shiro的会话管理
SecurityManager是shiro架构核心,协调内部安全组件(如登录,授权,数据源等),用来管理所有的subject
2.4.2自定义shiro会话管理
public class CustomSessionManger extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId= WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if(sessionId==null||sessionId=="")
{
//第一次登录
return super.getSessionId(request,response);
}else
{
//从哪里来
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
//值是什么
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
//要不要验证
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}
}
}
2.5自定义Realm
Realm(领域): Shiro从Realm获取安全数据(如用户、角色、权限)
public class UserRealm extends AuthorizingRealm{
@Override
public void setName(String name) {
super.setName("myShiroRealm");
}
@Autowired
private LoginService loginService;
/**
* 执行认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken.getPrincipal() == null) {
return null;
}
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
String userName = upToken.getUsername();
String md5PassWord = MD5Utils.MD5Low(new String(upToken.getPassword()));
// 可做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
UserShiro userShiro = loginService.selUserForName(userName);
if (userShiro == null) {
return null;
}
return new SimpleAuthenticationInfo(userShiro, userShiro.getPassWord(), getName());
}
@Autowired
RoleService roleService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
/*简单授权信息*/
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
/* Subject subject = SecurityUtils.getSubject();
UserLoginVO userLoginVO=(UserLoginVO)principalCollection.getPrimaryPrincipal();
*/ /*info.addStringPermission("delete");*/
/*info.addRole();*/
/*获取已认证的用户数据*/
UserShiro user = (UserShiro) principalCollection.getPrimaryPrincipal();
List<Menu> menus = roleService.selUserPermission(user.getUserId());
for (Menu menu : menus) {
info.addStringPermission(menu.getUrl());
for (MenuButton menuButton : menu.getMenBtn()) {
info.addStringPermission(menuButton.getRequestUrl());
}
}
// info.addRole();
return info;
}
/**
* 清楚缓存权限
*/
public void clearCachedAuthorizationInfo() {
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
2.6操作
2.6.1登录
/**
* 登录逻辑处理
*/
@PostMapping("/login")
@ResponseBody
@Override
public ResponseResult login(UserLoginDTO user, HttpServletRequest request) {
/**
* 使用Shiro编写认证操作
*/
//1.获取Subject
Subject subject = SecurityUtils.getSubject();
//2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassWord());
//3.执行登录方法
subject.login(token);
return new ResponseResult(CommonCode.SUCCESS);
}
2.6.2获取session
//获取session中的安全数据
Subject subject = SecurityUtils.getSubject();
//1.subject获取所有的安全数据集合
PrincipalCollection principals = subject.getPrincipals();
if (principals != null && !principals.isEmpty()) {
//2.获取安全数据
UserShiro result = (UserShiro) principals.getPrimaryPrincipal();
this.userId = result.getUserId();
this.userName = result.getUserName();
this.realName = result.getRealName();
}
3.分析Shiro源码
再了解一下shiro的基本架构
我们先进入口
subject.login(token);
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
我们可以看出里面又调用了securityManager.login(this, token);
我们先回顾一下securityManager最主要是用来干嘛的?
主要用来认证与鉴权
再来了解一下SecurityManager下面子类
DefaultSecurityManager
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
private Authenticator authenticator;
public AuthenticatingSecurityManager() {
super();
this.authenticator = new ModularRealmAuthenticator();
}
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
里面的authenticate(token); 是用来认证的,我们可以看出this.authenticator = new ModularRealmAuthenticator();
AbstractAuthenticator
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this " +
"Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
//Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more
//severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate:
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " +
"error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, t);
if (log.isWarnEnabled())
log.warn(msg, t);
}
try {
notifyFailure(token, ae);
} catch (Throwable t2) {
if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?. " +
"Please check your AuthenticationListener implementation(s). Logging sending exception " +
"and propagating original AuthenticationException instead...";
log.warn(msg, t2);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
notifySuccess(token, info);
return info;
}
doAuthenticate的实现在ModularRealmAuthenticator中
ModularRealmAuthenticator
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
我们主要看一下一个Realm的时候doSingleRealmAuthentication
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
调用了Realm的方法我们先了解一下Realm的子类
我们在看一下getAuthenticationInfo 获取身份验证信息
AuthenticatingRealm
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取缓存中的
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
//otherwise not cached, perform the lookup:
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}
if (info != null) {
assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}
return info;
}
我们先不关心缓存的,看doGetAuthenticationInfo
public class MyShiroRealm extends AuthorizingRealm {
@Override
public void setName(String name) {
super.setName("myShiroRealm");
}
/**
* 执行认证逻辑
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
/**
* 执行授权逻辑
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 清楚缓存权限
*/
public void clearCachedAuthorizationInfo() {
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
现在认证看懂了,授权是AuthorizingRealm
现在应该了解 Shiro的配置文件了