项目代码:https://gitee.com/starrk110/shiroRedisSpringboot.git
实现功能:shiro的认证及权限控制,缓存及session使用redis管理,remenberme功能。(框架基于SSM,使用了mybatis-plus增强)
1:主要使用jar包(见最后)
2:安装redis
- 安装redis及redis哨兵/集群部署请自行完成这不是重点
3:配置shiro的Configuration类
针对第一次接触shiro及springboot的人简单讲解一些常识,有了解的可以跳过:
- springboot采用@Configuration+@Bean的形式进行配置,相当于我们在spring中配置的各种bean。
- shiro是一个管理权限及认证的框架,主体是subject,通过在realm(你可以理解为内置可读写数据库的保险箱)中进行Authentication(认证,就是登陆),Authorization(授权,就是哪些页你能看)。其中还有各种管理器,比如缓存管理器CacheManager、会话管理器SessionManager等。通过这些管理器来进行shiro的个性化配置来达到符合你项目需求的目的。
开始配置
- shiro有其自己管理生命周期的类,各个bean需要Dependon这个类进行加载。
/**
* shiro管理生命周期的东西
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
- 我们首先要配置shiro所要使用的所有过滤器以及映射路径,在shiro中其内置了一些过滤器,比如anon(匿名)、logout(登出)、authc(认证)等等,如有个性化添加可以如下配置。其中的RedisTemplete是为了整合redis使用的。
/**
* 过滤器及映射路径的配置
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
RedisTemplate redisTemplate) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置拦截需要user/authc身份的跳转路径。
shiroFilterFactoryBean.setLoginUrl("/signin.html");
//配置登陆成功后跳转页面
shiroFilterFactoryBean.setSuccessUrl("/index.html");
//配置权限不足时跳转的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403.html");
//过滤器链
Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
// filters.put("perms", urlPermissionsFilter());
filters.put("logout", new MySignOutFilter(redisTemplate));
shiroFilterFactoryBean.setFilters(filters);
//权限映射链
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/403.html", "anon");
filterChainDefinitionMap.put("/signin.html", "anon");
filterChainDefinitionMap.put("/signout", "logout");
filterChainDefinitionMap.put("/**/delete*/**", "perms[Archer]");
filterChainDefinitionMap.put("/**/select*/**", "perms[Saber]");
filterChainDefinitionMap.put("/**/find*/**", "perms[Saber]");
filterChainDefinitionMap.put("/**/update*", "perms[Lancer]");
// filterChainDefinitionMap.put("/**/insert*", "perms[Berserker]");
filterChainDefinitionMap.put("/regist.html", "anon");
filterChainDefinitionMap.put("/contact.html", "authc,roles[天下无双]");
filterChainDefinitionMap.put("/post.html", "user,roles[Master]");
filterChainDefinitionMap.put("/about.html", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
- 之后配置安全管理模块,也是最为核心的一个模块,是为了配置你所有的manager
/**
* 安全管理模块,所有的manager在此配置
* @param redisTemplate
* @return
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(RedisTemplate redisTemplate) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//自定义realm
securityManager.setRealm(myShiroRealm(redisTemplate));
//自定义session管理 使用redis
securityManager.setSessionManager(sessionManager(redisTemplate));
// //自定义缓存实现 使用redis
// securityManager.setCacheManager(redisCacheManager());
//注入记住我管理器;
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
- 这时候我们需要配置一个认证授权模块了,也是最关键的Realm,这个realm需要我们自己去手写其实现方式,来判断你所使用的加密算法,以及你的角色授权,通过继承AuthorizingRealm这个类。
然后去配置这个自己实现的类,自己实现的类如下
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private MPTbBloggerService tbBloggerService;
public MyShiroRealm(){
}
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String)principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//权限获取
List<Authority> authorities = tbBloggerService.findAuthorityByUsername(username);
Set<String> permisssionSets = new HashSet<>();
for (Authority perm:authorities) {
permisssionSets.add(perm.getAuthorName());
}
info.setStringPermissions(permisssionSets);
//角色获取
List<Role> roles = tbBloggerService.findRolesByUsername(username);
Set<String> rolenames = new HashSet<>();
for (Role role:roles) {
rolenames.add(role.getRoleName());
}
info.addRoles(rolenames);
return info;
}
/**
* 认证
* @param Atoken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken Atoken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)Atoken;
String username = token.getUsername();
//这里的获取对象是MybatisPlus中的写法,如果不适可以使用mybatis手写。
TbBlogger tbBlogger = tbBloggerService.selectOne(new EntityWrapper<TbBlogger>().where("username={0}",username));
if (tbBlogger == null) {
throw new UnknownAccountException();
}
String password =tbBlogger.getPassword();
// ByteSource.Util.bytes(tbBlogger.getSalt()),简单讲解一下这里,
//对于第一次接触shiro的人来说应该是最难理解的地方,这个SimpleAuthenticationInfo会将你Token中的账号密码通过getName()
//这个方法获取,与你传入的username及password进行对比,byteSource是盐值,
//是为了加密时使用的。这里我采用了盐值存储在用户信息中的方式,而盐值的设置是在
//我注册用户时设置的,我采用的是随机字符串形式,当然你也可以采用随机数格式。
//而这个解密的方式在哪配置的,请看后面。
SimpleAuthenticationInfo AInfo = new SimpleAuthenticationInfo(username,password, ByteSource.Util.bytes(tbBlogger.getSalt()),getName());
return AInfo;
}
}
配置这个类在Configure中
@Bean(name = "myShiroRealm")
@DependsOn(value = {"lifecycleBeanPostProcessor", "ShiroRedisCacheManager"})
public MyShiroRealm myShiroRealm(RedisTemplate redisTemplate) {
MyShiroRealm shiroRealm = new MyShiroRealm();
//设置缓存管理器
shiroRealm.setCacheManager(redisCacheManager(redisTemplate));
shiroRealm.setCachingEnabled(true);
//设置认证密码算法及迭代复杂度
shiroRealm.setCredentialsMatcher(credentialsMatcher());
//认证
shiroRealm.setAuthenticationCachingEnabled(false);
//授权
shiroRealm.setAuthorizationCachingEnabled(false);
return shiroRealm;
}
这时候我们就要配置那个加密算法了,当然注册时的加密算法需要你在注册的Controller里写,可以是前台写,可以是后台写,反正是post提交
只要加上这句
//算法,密码,盐值,迭代次数。
SimpleHash password = new SimpleHash("md5",tbBlogger.getPassword(), ByteSource.Util.bytes(tbBlogger.getSalt()),2);
/**
* realm的认证算法
* @return
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
//2次迭代
credentialsMatcher.setHashIterations(2);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
- 至此其实基本的shiro框架已经配置完成了,但是由于需要整合redis及将shiro的session交由redis管理,才有以下的配置,
- 配置CacheManager
/**
* 缓存管理器的配置
* @param redisTemplate
* @return
*/
@Bean(name = "ShiroRedisCacheManager")
public ShiroRedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
ShiroRedisCacheManager redisCacheManager = new ShiroRedisCacheManager(redisTemplate);
//name是key的前缀,可以设置任何值,无影响,可以设置带项目特色的值
redisCacheManager.createCache("shiro_redis");
return redisCacheManager;
}
这个manager由于要使用redis进行存储,所以需要我们自己来实现,当然也很简单。如下
public class ShiroRedisCacheManager extends AbstractCacheManager{
private RedisTemplate<byte[],byte[]> redisTemplate;
public ShiroRedisCacheManager(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
//为了个性化配置redis存储时的key,我们选择了加前缀的方式,所以写了一个带名字及redis操作的构造函数的Cache类
@Override
protected Cache createCache(String name) throws CacheException {
return new ShiroRedisCache(redisTemplate,name);
}
}
个性化的Cache
public class ShiroRedisCache<K,V> implements Cache<K,V> {
private RedisTemplate redisTemplate;
private String prefix = "shiro_redis:";
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public ShiroRedisCache(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
public ShiroRedisCache(RedisTemplate redisTemplate,String prefix){
this(redisTemplate);
this.prefix = prefix;
}
@Override
public V get(K k) throws CacheException {
if (k == null) {
return null;
}
byte[] bytes = getBytesKey(k);
return (V)redisTemplate.opsForValue().get(bytes);
}
@Override
public V put(K k, V v) throws CacheException {
if (k== null || v == null) {
return null;
}
byte[] bytes = getBytesKey(k);
redisTemplate.opsForValue().set(bytes, v);
return v;
}
@Override
public V remove(K k) throws CacheException {
if(k==null){
return null;
}
byte[] bytes =getBytesKey(k);
V v = (V)redisTemplate.opsForValue().get(bytes);
redisTemplate.delete(bytes);
return v;
}
@Override
public void clear() throws CacheException {
redisTemplate.getConnectionFactory().getConnection().flushDb();
}
@Override
public int size() {
return redisTemplate.getConnectionFactory().getConnection().dbSize().intValue();
}
@Override
public Set<K> keys() {
byte[] bytes = (prefix+"*").getBytes();
Set<byte[]> keys = redisTemplate.keys(bytes);
Set<K> sets = new HashSet<>();
for (byte[] key:keys) {
sets.add((K)key);
}
return sets;
}
@Override
public Collection<V> values() {
Set<K> keys = keys();
List<V> values = new ArrayList<>(keys.size());
for(K k :keys){
values.add(get(k));
}
return values;
}
private byte[] getBytesKey(K key){
if(key instanceof String){
String prekey = this.prefix + key;
return prekey.getBytes();
}else {
return SerializeUtil.serialize(key);
}
}
}
- 我们将session交由Redis去管理,这个时候就需要进行redis的写入,所以需要一个redisSessionDao,写入的key使用我们自己生成一个个性化的key,设置为SessionId,当然你完全可以使用JDK的那个UUID,然后Value的值为session的内容。
/**
* 配置sessionmanager,由redis存储数据
*/
@Bean(name = "sessionManager")
@DependsOn(value = "lifecycleBeanPostProcessor")
public DefaultWebSessionManager sessionManager(RedisTemplate redisTemplate) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
MyRedisSessionDao redisSessionDao = new MyRedisSessionDao(redisTemplate);
//这个name的作用也不大,只是有特色的cookie的名称。
redisSessionDao.setSessionIdGenerator(sessionIdGenerator("starrkCookie"));
sessionManager.setSessionDAO(redisSessionDao);
sessionManager.setDeleteInvalidSessions(true);
SimpleCookie cookie = new SimpleCookie();
cookie.setName("starrkCookie");
sessionManager.setSessionIdCookie(cookie);
sessionManager.setSessionIdCookieEnabled(true);
return sessionManager;
}
/**
* 自定义的SessionId生成器
* @param name
* @return
*/
public MySessionIdGenerator sessionIdGenerator(String name) {
return new MySessionIdGenerator(name);
}
我们实现的sessionDao
public class MyRedisSessionDao extends EnterpriseCacheSessionDAO {
private RedisTemplate<byte[],byte[]> redisTemplate;
public MyRedisSessionDao(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
redisTemplate.opsForValue().set(sessionId.toString().getBytes(),sessionToByte(session));
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = super.doReadSession(sessionId);
if(session == null){
byte[] bytes = redisTemplate.opsForValue().get(sessionId.toString().getBytes());
if(bytes != null && bytes.length > 0){
session = byteToSession(bytes);
}
}
return session;
}
//设置session的最后一次访问时间
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
redisTemplate.opsForValue().set(session.getId().toString().getBytes(),sessionToByte(session));
}
// 删除session
@Override
protected void doDelete(Session session) {
super.doDelete(session);
redisTemplate.delete(session.getId().toString().getBytes());
}
private byte[] sessionToByte(Session session){
if (null == session){
return null;
}
ByteArrayOutputStream bo = new ByteArrayOutputStream();
byte[] bytes = null;
ObjectOutputStream oo ;
try {
oo = new ObjectOutputStream(bo);
oo.writeObject(session);
bytes = bo.toByteArray();
}catch (Exception e){
e.printStackTrace();
}
return bytes;
}
private Session byteToSession(byte[] bytes){
if(0==bytes.length){
return null;
}
ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
ObjectInputStream in;
SimpleSession session = null;
try {
in = new ObjectInputStream(bi);
session = (SimpleSession) in.readObject();
}catch (Exception e){
e.printStackTrace();
}
return session;
}
}
- 然后我们配置了记住我功能
/**
* 这个参数是RememberMecookie的名称,随便起。
* remenberMeCookie是一个实现了将用户名保存在客户端的一个cookie,与登陆时的cookie是两个simpleCookie。
* 登陆时会根据权限去匹配,如是user权限,则不会先去认证模块认证,而是先去搜索cookie中是否有rememberMeCookie,
* 如果存在该cookie,则可以绕过认证模块,直接寻找授权模块获取角色权限信息。
* 如果权限是authc,则仍会跳转到登陆页面去进行登陆认证.
* @return
*/
public SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("remenbermeCookie");
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(60);
return simpleCookie;
}
/**
* cookie管理对象;记住我功能
*/
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
至此,全部的springboot整合redis及shiro的配置类@Configuration就写完了。
- *
调用
我们在登陆时进行了调用
主要通过前面讲的主体SecurityUtils.getSubject().login(token)来进行登陆。
具体实现如下。
/**
* 通过用户名和密码查询用户信息
* @param loginVo
* @param request
* @return
*/
@PostMapping("/signinbyUAP")
//封装了一个Vo来接收username,password,rememberme属性
public String getTbBloggerInfo(LoginVo loginVo, HttpServletRequest request){
System.out.println("登陆时的Vo:"+loginVo);
UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getUsername(),loginVo.getPassword(),loginVo.isRemenberMe());
try {
SecurityUtils.getSubject().login(token);
return "index";
}catch(Exception e) {
request.setAttribute("wrongMessage",e.getMessage());
return "signin.html";
}
}
jar包
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.1.9</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.1.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>