Shiro的基本知识 + 本次示例的项目代码 + 两分钟测试视频 在本文末给出了下载链接。
软硬件环境:JDK1.8、Eclipse、Shiro1.3.2。
提示
-
本节知识中,本意是分享前后端分离的Shiro用法的;但是为了更直观,所以本 人把几个需要用到的html文件引入到了项目中;在实际前后端分离时,可以只对后端URI进行Shiro验证,前端是否验证等交给前段工作人员处理即可。
-
Session持久化最好放入redis中间件中储存,为了方便快速示例,本人这里直接将session存入了数据库中。
准备工作
第一步:在pom.xml中引入Shiro相关依赖
<!-- shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- shiro-ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
第二步:引入缓存配置文件、日志配置文件
注:缓存配置是必须的,这里采用引入配置文件的方式来配置缓存。
注:关于logback日志框架的使用,本节不在赘述,可参考这里。
给出ehcache.xml中的配置内容:
<ehcache>
<diskStore path="java.io.temp"/>
<cache name="authorizationCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="authenticationCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<cache name="shiro-activeSessionCache"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="true"
/>
<cache name="sampleCache1"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<cache name="sampleCache2"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/>
</ehcache>
第三步:准备等下需要用到的用来方便示例的html页面
注:除了login.html页面有内容外,其余页面几乎没有内容,只有形如“User Page”这样的几个字,用来告诉你进入了什么页面。
给出login.html页面内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Shiro</title>
</head>
<body>
<h4>登录页面</h4>
<form action="login" method="POST">
username: <input type="text" name="username" />
<br />
password: <input type="password" name="password" />
<br />
<input type="submit" value="Submit" />
</form>
<hr>
<a href="superadmin.html">superadmin</a>
<br>
<a href="admin.html">Admin Page</a>
<br>
<a href="user.html">User Page</a>
<br>
<a href="test">Test</a>
<br>
<a href="rememberMe.html">RememberMe</a>
<br>
<a href="introduce.html">introduce</a>
<br>
<a href="logout">Logout</a>
</body>
</html>
第四步:创建存放session的表
CREATE TABLE `sessions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`jsessionid` varchar(200) DEFAULT NULL,
`session` varchar(4000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=266 DEFAULT CHARSET=utf8;
注:如果不需要session持久化的话,可忽略此步。
代码总体说明
SessionPermanentClass:对session持久化进行一定的逻辑处理,与SessionCRUDMapper搭配使用。即:相当于serviceImpl,由于其特殊性所以没按Impl命名。
ShiroConfigure:Shiro的核心配置。
ShiroRealm:Shiro的认证、授权配置。
ShiroHandler:一个用户登录的Controller,由于其特殊性,所以这里这么命名。
TestController:用于下面进行身份认证测试的后端URI对应的Conytroller。
SessionCRUDMapper:通过Java注解的方式操作数据。
User:用户信息model。
SerializableUtils:session序列化工具。
代码详细说明
提示:如果不需要session持久化的话,那么在ShiroConfigure中不要作session的引用、配置等即可。
这里按照上面列举的顺序,依次给出:
SessionPermanentClass:对session持久化进行一定的逻辑处理,与SessionCRUDMapper搭配使用。即:相当于serviceImpl,由于其特殊性所以没按Impl命名。
import java.io.Serializable;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import com.aspire.mapper.SessionCRUDMapper;
import com.aspire.util.SerializableUtils;
/**
* session增删改查
*
* @author JustryDeng
* @Date 2018年8月24日 下午5:44:19
*/
public class SessionPermanentClass extends EnterpriseCacheSessionDAO {
@Autowired
private SessionCRUDMapper sessionCRUDMapper;
@Override
protected Serializable doCreate(Session session) {
Serializable jsessionId = generateSessionId(session);
assignSessionId(session, jsessionId);
// 增
sessionCRUDMapper.doCreate((String) jsessionId, SerializableUtils.serialize(session));
return jsessionId;
}
@Override
protected Session doReadSession(Serializable jsessionId) {
// 查
String sessionString = sessionCRUDMapper.doReadSession((String) jsessionId);
if (sessionString == null) {
return null;
}
return SerializableUtils.deserialize(sessionString);
}
@Override
protected void doUpdate(Session session) {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
return;
}
Serializable jsessionId = generateSessionId(session);
if (jsessionId != null) {
// 改
sessionCRUDMapper.doUpdate(SerializableUtils.serialize(session), (String) jsessionId);
}
}
@Override
protected void doDelete(Session session) {
Serializable jsessionId = generateSessionId(session);
if (jsessionId != null) {
// 删
sessionCRUDMapper.doDelete((String) jsessionId);
}
}
}
ShiroConfigure:Shiro的核心配置。
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
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 org.springframework.web.filter.DelegatingFilterProxy;
/**
* Shiro配置
* 提示:@Bean注解默认采用方法名作为id
*
* @author JustryDeng
* @Date 2018年8月23日 上午10:58:22
*/
@Configuration
public class ShiroConfigure {
/**
* 注入Shiro的 [核心]管理器 配置
*
* @Date 2018年8月23日 下午2:03:41
*/
@Bean
@DependsOn(value = { "authenticator", "ehCacheManager" })
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager dsm = new DefaultWebSecurityManager();
// 将cacheManager引入securityManager
dsm.setCacheManager(ehCacheManager());
// 将rememberMeManager引入securityManager
dsm.setRememberMeManager(rememberMeManager());
// 将authenticator引入securityManager
dsm.setAuthenticator(authenticator());
// 将sessionManager引入securityManager
dsm.setSessionManager(sessionManager());
// 将realms引入securityManager
Collection<Realm> realms = new ArrayList<>(8);
realms.add(shiroRealm());
dsm.setRealms(realms);
return dsm;
}
/* ...........................华丽分割线........................... */
/**
* 注入 自定义ShiroRealm类的实例
*
* @Date 2018年8月23日 下午5:41:11
*/
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
/*
* 比对用户输入的明文密码 与 数据库提前存起来的加密后密码时,将用户输入的 明文密码进
* 行MD5、迭代1024次后再比对(这里具体的加密规则要与数据库中的已经加密了的密码的加密规则一致)
* 注:盐值 在重写认证器的时候设置
*
*/
// 设置加密算法为MD5
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置加密迭代次数为1024次
credentialsMatcher.setHashIterations(1024);
shiroRealm.setCredentialsMatcher(credentialsMatcher);
return shiroRealm;
}
/* ...........................华丽分割线........................... */
/**
* 认证器
*
* @Date 2018年8月23日 下午4:26:38
*/
@Bean
public ModularRealmAuthenticator authenticator() {
ModularRealmAuthenticator mra = new ModularRealmAuthenticator();
/*
* AuthenticationStrategy接口有三种实现
* 这里以AtLeastOneSuccessfulStrategy作为身份认证策略
*/
AuthenticationStrategy as = new AtLeastOneSuccessfulStrategy();
mra.setAuthenticationStrategy(as);
return mra;
}
/* ...........................华丽分割线........................... */
/**
* 注入简单cookie配置
*
* @Date 2018年8月23日 下午2:06:35
*/
@Bean
public SimpleCookie rememberMeCookie() {
SimpleCookie sc = new SimpleCookie();
// 设置Cookie在客户端浏览器中保存内容的cookie的名字
sc.setName("rememberMe");
// 证该系统不会受到跨域的脚本操作攻击
sc.setHttpOnly(true);
// 定义该Cookie的过期时间,单位为秒。如果设置为-1标识浏览器关闭就失效
sc.setMaxAge(60);
return sc;
}
/**
* 注入remembernMe的Cookie的管理器 配置
*
* @Date 2018年8月23日 下午2:15:25
*/
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager crmm = new CookieRememberMeManager();
// 设置RememberMe的加密后Cookie的cipherKey
byte[] cipherKey = Base64.decode("4AvVhmFLUs0KTA3Kprsdag==");
crmm.setCipherKey(cipherKey);
// cookie配置引用上面的SimpleCookie
crmm.setCookie(rememberMeCookie());
return crmm;
}
/* ...........................华丽分割线........................... */
/**
* 注入shiro bean的生命周期处理器。 可以自动调用配置在 Spring IOC容器中shiro bean的生命周期方法
*
* @Date 2018年8月23日 下午3:06:13
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
LifecycleBeanPostProcessor lbpp = new LifecycleBeanPostProcessor();
return lbpp;
}
/**
* 使IOC容器中的bean可以使用 shiro的注解. @DependsOn注解可保证在注入此bean之前,会先注入指定的bean
*
* {@code BeanPostProcessor} implementation that creates AOP proxies based on
* all candidate {@code Advisor}s in the current {@code BeanFactory}. This class
* is completely generic; it contains no special code to handle any particular
* aspects, such as pooling aspects.
*
* @Date 2018年8月23日 下午3:15:13
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daapc = new DefaultAdvisorAutoProxyCreator();
return daapc;
}
/**
* Convenient base class for Advisors that are also static pointcuts.
*
* @Date 2018年8月23日 下午3:21:45
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
// 引用上面的securityManager()配置
aasa.setSecurityManager(securityManager());
return aasa;
}
/* ...........................华丽分割线........................... */
/**
* 注入ShiroFileter
*
* @Date 2018年8月23日 下午3:42:55
*/
@Bean
@DependsOn(value = { "securityManager" })
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean sffb = new ShiroFilterFactoryBean();
/*
* Sets the application {@code SecurityManager} instance to be
* used by the constructed Shiro Filter
*/
sffb.setSecurityManager(securityManager());
// 当Shiro验证时,如果不满足认证条件,那么打回到这个页面
sffb.setLoginUrl("/login.html");
// 当Shiro认证(登陆)成功后,默认跳转到的页面
sffb.setSuccessUrl("/login.html");
// 不满足权限条件,那么跳转至此页面
sffb.setUnauthorizedUrl("/unauthorized.html");
/* ----------------------下面配置:拦截器(过滤器)---------------------- */
// urlPathExpression_to_comma-delimited-filter-chain-definition
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("/login.html", "anon");
map.put("/login", "anon");
// 当请求/shiro/logout时登出
map.put("/logout", "logout");
/*
* 注意:这里的user是过滤器的一种,而下面roles[user]中的user是自定义的一种角色。
* 注意:user拦截器既允许通过Subject.login()认证进入的用户访问;又允许通过rememberMe缓存进入的用户访问
* 注意:authc拦截器既只允许通过Subject.login()认证进入的用户访问;不允许rememberMe缓存通过进入的用户访问
*/
map.put("/introduce.html", "user");
map.put("/rememberMe.html", "user");
// 注意roles[user]这里的话,角色不要再用引号引起来了,直接写即可
map.put("/user.html", "authc,roles[user]");
map.put("/admin.html", "authc,roles[admin]");
map.put("/superadmin.html", "authc,roles[host,admin]");
// 由于权限由上而下“就近”选择,所以一般将"/**"配置在最下面;还有一些细节,可详见《程序员成长笔记(三)》相关章节
map.put("/**", "authc");
sffb.setFilterChainDefinitionMap(map);
return sffb;
}
/**
* 启用配置上面配置的shiro拦截器
*
* @Date 2018年8月23日 下午3:35:51
*/
@Bean
@DependsOn(value = { "shiroFilter" })
public DelegatingFilterProxy shiroFilterProxy() {
DelegatingFilterProxy dfp = new DelegatingFilterProxy();
// 引用上面的shiroFilter
dfp.setTargetBeanName("shiroFilter");
return dfp;
}
/* ...........................华丽分割线........................... */
/**
* 注入EhCacheManager缓存管理器
*
* @Date 2018年8月23日 下午4:04:40
*/
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ecm = new EhCacheManager();
ecm.setCacheManagerConfigFile("classpath:ehcache.xml");
return ecm;
}
/* ...........................华丽分割线........................... */
/**
* 注入SessionId生成器
*
* @Date 2018年8月24日 下午4:26:22
*/
@Bean
public JavaUuidSessionIdGenerator sessionIdGenerator() {
JavaUuidSessionIdGenerator jusi = new JavaUuidSessionIdGenerator();
return jusi;
}
/**
* 注入自己编写的持久化Session的类
*
* @Date 2018年8月24日 下午4:26:22
*/
@Bean
@DependsOn(value = { "sessionIdGenerator" })
public SessionPermanentClass sessionDAO() {
SessionPermanentClass jusi = new SessionPermanentClass();
jusi.setSessionIdGenerator(sessionIdGenerator());
// 设置使用哪一个 缓存
jusi.setActiveSessionsCacheName("shiro-activeSessionCache");
return jusi;
}
/**
* 注入会话管理器
*
* @Date 2018年8月24日 下午4:26:22
*/
@Bean
@DependsOn(value = { "sessionDAO" })
public DefaultSessionManager sessionManager() {
// 这里创建实例时,要用DefaultWebSessionManager;而不要用DefaultSessionManager
DefaultSessionManager dsm = new DefaultWebSessionManager();
// 设置session失效时间为30分钟
dsm.setGlobalSessionTimeout(1800000);
// 是否定时检查session失效没有
dsm.setSessionValidationSchedulerEnabled(true);
// 如果session失效了,那么删除失效了的session
dsm.setDeleteInvalidSessions(true);
// 指定引用上面配置的sessionDAO
dsm.setSessionDAO(sessionDAO());
return dsm;
}
}
ShiroRealm:Shiro的认证、授权配置。
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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 org.springframework.context.annotation.Configuration;
import com.aspire.model.User;
/**
* Realms配置;
* 注:可编写多个realm类继承AuthorizingRealm实现多认证、多授权;
* 为快速实例,这里只编写了一个类来继承AuthorizingRealm
*
* @author JustryDeng
* @Date 2018年8月25日 上午11:00:31
*/
@Configuration
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println(".......................................ShiroRealm");
// 1. 把 AuthenticationToken 拆箱转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
/*
* 3. 调用数据库的方法, 从数据库中查询 username对应的用户记录
* 注:一般的 , 用户名 什么的 最好唯一
* 4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
* 5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
* 6. principal:认证的用户实体信息.(可以为 username、手机号、邮箱等,也可以是一个携带用户信息的对象模型)
* 注:也可以是数据表对应的用户的实体类对象,在鉴权时可以冲这个对象中回去到其对应有哪些权限.
* 注:用户对象信息本应该是从数据库中查询出来的,这里为了快速测试,直接new一个
* 注:用于存放用户信息的模型,必须能后实例化。即:必须实现Serializable接口
* 注: 这里假设从数据库查出来了某个用户的数据,假设User类的实例principal中的就是查出来的数据
* 注:如果我们想要在程序中,获取到我们在Realm里面方式的自定义的用户对象实例(即上图中的User principal),那么可以这么获得:
* User u = (User)SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
*/
User principal = null ;
if(username != null && username.equals("zhangsan")) {
principal = new User(username, "2a0d136ceacafe198ea64ac09daaf1b6", "admin,user");
}
if(username != null && username.equals("lisi")) {
principal = new User(username, "8c702ae443795331c91cfab48f3f3833", "user");
}
/*
* 7.credentials: 凭证(一般都是密码).
* credentials本应该是查询出来的;这里为了快速测试,我们直接写
*/
Object credentials = principal.getPassword();
/*
* 8.realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
* 这里获取到的是:com.aspire.shiro.realms.ShiroRealm_0
*/
String realmName = getName();
/*
* 9. 盐值.
* 注:如果多个用户的密码一样,那么一般情况下加密结果也一样;
* 注:通过使用不同的盐值来确保即便密码都一样,加密结果也会不一样
* 注:盐值 最好保证其唯一性。
* 注:由于一般情况下,用户名是唯一的,所以我们一般使用用户名来计算盐值
*/
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
/*
* 实例化对象.
* 注意:如果不加密,那么就是直接比对的明文
* 注意:SimpleAuthenticationInfo加密,是指:将用户登录时输入的密码(盐值)加密后,与数据库取出来的密码进行比对
* 所以,这里的加密并不是对从数据库取出来的credentials进行加密!从数据库取出来的credentials应该是
* 之前录入数据库时已经加密好了的.
*/
SimpleAuthenticationInfo info = null;
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("...................ShiroRealm鉴权");
/*
* 1.从principals中获取到第一个principal
* 注:多个Realm时,Shiro返回给此方法的参数principals的规则是由身份认证策略控制的(可详见:身份认证策略)
* 注:默认的策略为AtLeastOneSuccessfulStrategy,那么这里只能获取
* 到对应的通过了身份认证的Realm中的principal
* 注:就算有多个Realm,一般而言这些Realm中的principal都应该是一样的,所以
* 我们读取信息时,一般拿其中的某一个进行读取就行
*/
Object principal = principals.getPrimaryPrincipal();
/*
* 2.从配置applicationContext.xml中可知:我们写的ShiroReaml是第一个,
* 而ShiroReaml中的principal,我们传的是User对象
* 所以这里获取到的principal即为该User对象,并获取到对应其角色信息
*/
User user = (User)principal;
// 获取该用户(带的)角色信息
String[] roles = null;
if(user.getUserRoles() != null) {
roles = (user.getUserRoles()).split(",");
}
// 3.创建一个Set,来放置用户拥有的角色
java.util.Set<String> rolesSet = new java.util.HashSet<>();
for (String role : roles) {
rolesSet.add(role);
}
// 4.创建 SimpleAuthorizationInfo, 并将办好角色的Set放入.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(rolesSet);
// 5.返回 SimpleAuthorizationInfo对象.
return info;
}
}
///**
//* 我们可以这样获取:盐值加密后的结果
//*
//* @param args
//* @date 2018年8月15日 上午11:46:23
//*/
//public static void main(String[] args) {
// // 加密算法(MD5、SHA1等)
// String hashAlgorithmName = "MD5";
// // 加密对象(即:密码)
// Object credentials = "123456";
// // 盐值(注:一般使用用户名)
// Object salt = ByteSource.Util.bytes("zhangsan");
// // 迭代次数
// int hashIterations = 1024;
//
// // 执行加密
// Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
// System.out.println(result);
//}
提示:如果我们想要在程序中,获取到我们在Realm里面方式的自定义的用户对象实例(即上图中的User principal),
那么可以这么获得:
ShiroHandler:一个用户登录的Controller,由于其特殊性,所以这里这么命名。
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ShiroHandler {
private static final Logger logger = LoggerFactory.getLogger(ShiroHandler.class);
/**
* 用户登录入口
*
* @Date 2018年8月25日 下午12:25:21
*/
@RequestMapping("login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
String flag = "success";
// 当前用户
Subject currentUser = SecurityUtils.getSubject();
// 验证是否身份认证֤
if (!currentUser.isAuthenticated()) {
// 将用户名、密码封装为token
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// rememberMe
token.setRememberMe(true);
try {
// 执行登录
currentUser.login(token);
} catch (UnknownAccountException uae) {
logger.error("There is no user with username of " + token.getPrincipal());
flag = "fail";
} catch (IncorrectCredentialsException ice) {
logger.error("Password for account " + token.getPrincipal() + " was incorrect!");
flag = "fail";
} catch (LockedAccountException lae) {
logger.error("The account for username " + token.getPrincipal() + " is locked. "
+ "Please contact your administrator to unlock it.");
flag = "fail";
} catch (AuthenticationException ae) {
logger.error("authentication fail!");
flag = "fail";
}
}
return flag;
}
}
TestController:用于下面进行身份认证测试的后端URI对应的Conytroller。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用于测试的Controller
*
* @author JustryDeng
* @Date 2018年8月25日 上午11:09:53
*/
@RestController
public class TestController {
@Value("${spring.datasource.username}")
private String datasourceUsername;
/**
* 当用户认证后;满足ShiroConfigure中配置的放行条件时,可访问"/test"
*
* @Date 2018年8月25日 上午11:10:49
*/
@GetMapping("/test")
public String testMethod() {
String string = "配置文件中的参数是:" + datasourceUsername;
return string;
}
}
SessionCRUDMapper:通过Java注解的方式操作数据。
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
/**
* session增删改查 持久化接口
*
* @author JustryDeng
* @Date 2018年8月24日 下午5:10:58
*/
@Mapper
public interface SessionCRUDMapper {
/**
* 增
*
* @param jsessionId
* @param sessionString
* 使用序列化工具转换为字符串后的session
* @return 受影响行数
* @Date 2018年8月24日 下午5:11:26
*/
@Insert("insert into sessions(jsessionid, session) values(#{jsessionId},#{sessionString})")
int doCreate(@Param("jsessionId") String jsessionId, @Param("sessionString") String sessionString);
/**
* 查
*
* @param jsessionId
* @return 使用序列化工具转换为字符串后的session
* @Date 2018年8月24日 下午5:14:55
*/
@Select("select session from sessions where jsessionid=#{jsessionId}")
String doReadSession(@Param("jsessionId") String jsessionId);
/**
* 改
*
* @return 受影响行数
* @Date 2018年8月24日 下午5:15:33
*/
@Update("update sessions set session=#{sessionString} where jsessionid=#{jsessionId}")
int doUpdate(@Param("sessionString") String sessionString, @Param("jsessionId") String jsessionId);
/**
* 删
*
* @return 受影响行数
* @Date 2018年8月24日 下午5:15:35
*/
@Delete("delete from sessions where jsessionid=#{jsessionId}")
int doDelete(@Param("jsessionId") String jsessionId);
}
User:用户信息model。
import java.io.Serializable;
/**
* 用户模型
* 注意:此模型必须能够序列化,即:必须implements Serializable
*
* @author JustryDeng
* @Date 2018年8月23日 下午4:35:44
*/
public class User implements Serializable{
/** 序列化UID为-4914585368925337032L */
private static final long serialVersionUID = -4914585368925337032L;
/** 用户名 */
private String username;
/** 用户密码 */
private String password;
/** 用户角色 */
private String userRoles;
/**
* 无参构造
*/
public User() {
}
/**
* 全参构造
*/
public User(String username, String password, String userRoles) {
super();
this.username = username;
this.password = password;
this.userRoles = userRoles;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUserRoles() {
return userRoles;
}
public void setUserRoles(String userRoles) {
this.userRoles = userRoles;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("usrename : " + username);
sb.append(",password : " + password);
sb.append(",userRoles : " + userRoles);
return sb.toString();
}
}
SerializableUtils:session序列化工具。
import org.apache.shiro.codec.Base64;
import org.apache.shiro.session.Session;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* Session序列化工具
*/
public class SerializableUtils {
public static String serialize(Session session) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(session);
return Base64.encodeToString(bos.toByteArray());
} catch (Exception e) {
throw new RuntimeException("serialize session error", e);
}
}
public static Session deserialize(String sessionStr) {
try {
ByteArrayInputStream bis = new ByteArrayInputStream(
Base64.decode(sessionStr));
ObjectInputStream ois = new ObjectInputStream(bis);
return (Session) ois.readObject();
} catch (Exception e) {
throw new RuntimeException("deserialize session error", e);
}
}
}
测试一下
给出测试流程说明
本人在ShiroConfigure中配置了对哪些路径采用哪一个拦截器;当用户登录时,会进入ShiroHandler中,根据用户的登录名等信息,将这个用户的其他信息(如:角色等)从数据库中查出来,放入User中(这里本人为了快速示例,直接写的用户信息zhangsan、lisi,并赋予了一些角色权限给他们);登录前,我们是访问不了需要authc和需要一些权限的资源的(包括今天html资源、后端URI等都算),登录(认证)之后,我们能访问部分资源了,如果访问那些有权限要求的资源,那么在AOP中,会验证当前登陆者是否具备对应的权限(即:角色),如果具备了那么就能访问,不具备的话,那么就会跳转到设置好了的“没有权限的提示”地址。
注:那些页面有哪些要求,可详见ShiroConfigure中的shiroFilter配置。
提示:本人直接录制了一个测试视频,与代码放在一块儿。