学习springboot的旅程,就像蜗牛爬山,一点点的往上爬,一点点的欣赏旅途的风景
继续上一章的故事,听说小猿公司要加人,同时又要搞手机端。小猿此刻头大得像个灯泡一样能照亮人间!然后小猿把自己锁进了洗手间,蹲在坑上冥想,性能提升+手机端接入所面临的问题:
- 服务集群:问题身份验证信息的共享问题?
- 手机端:没session这个概念?怎么同时兼任解决手机及PC的身份安全验证?
大约几小时后,小猿在众人奇异目光下,兴奋的冲出洗手间,因为他想到了shiro+redis+服务无状态
服务无状态:支业务系统不存储session信息
shiro:安全管理框架,教程请看这
redis:用来共享信息的,比如身份信息
springboot+shiro+redis+服务无状态
- 第一步:pom.xml引入相关配置
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
- 第二步:在application.properties(application.ymx)配置
#redis
#请看上一章的redis第二步配置
#shiro
spring.redis.shiro.timeout=0
- 第三步:实现Realm【继承接口,extends AuthorizingRealm】
import com.example.hxzboot.Dome.Sys.Core.Service.CoreService;
import com.example.hxzboot.Dome.Sys.User.Service.UserService;
import com.example.hxzboot.Dome.Util.UtilClass;
import org.apache.shiro.SecurityUtils;
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 org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
/**
* 【AuthorizingRealm】接口提供了两个方法(验证权限和验证身份-->自己定义验证逻辑)
* 1-protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollectionprincipalCollection)用来验证权限(授权的)
* 2-protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationTokenauthenticationToken)throws AuthenticationException 用来身份验证的
**/
public class HxzRealm extends AuthorizingRealm {
//此处注入数据库连接,可通过数据库验证用户和权限验证
@Autowired
private CoreService coreService;
/**
* 授权--用户权限认证
* 此处要考虑前后分离和前后不分离的情况
* 前后分离:请求都是跨域ajax
* 前后不分离:则是普通请求
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//假设用户拥有的权限--此处我是写死的,后期加权限时可补上
Set<String> stringSet = new HashSet<>();
stringSet.add("log/initlog");
info.setStringPermissions(stringSet);//shiro会将权限集合跟请求地址进行匹配,来验证权限
return info;
}
// 属于user角色@RequiresRoles("user")
// 必须同时属于user和admin角@RequiresRoles({ "user", "admin" })
//修饰controller方法的标签--被这标签修饰的方法是被shiro权限管理,会进行权限认证(没有的者不进入权限认证)
//@RequiresRoles(value = { "user", "admin"})
//@RequiresPermissions("user:query")
/**
* 认证--用户身份验证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String userName = (String) authenticationToken.getPrincipal();
String userPwd = new String((char[]) authenticationToken.getCredentials());//页面的传过来的密码被加密后的
//假设已经在数据库获取加密的密码
String password = "1bb2957bf9db25cb4a64ea4023ec8301";//用hxz和123加密的
if (userName == null) {
throw new AccountException("用户名不正确");
}
return new SimpleAuthenticationInfo(userName, password, ByteSource.Util.bytes(userName + "salt"),getName());
}
}
- 第四步:【重写WebSessionManager管理器】【实现无状态管理】–【如果不需要前后分离,可不用此步骤】
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
public class HxzSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public HxzSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
- 第五步:【核心配置】–【怎么配置已经在方法上说明了】
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.session.mgt.SessionManager;
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.mgt.SecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig implements WebMvcConfigurer {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.shiro.timeout}")
private int timeout;
/**
* 配置shiro的过滤链,过滤不拦截的
* @return
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//shiroFilterFactoryBean.setLoginUrl("/restlogin");//前后端分离的情况下是不能设置固定登陆url的
//shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->//---这一块要动态化(加入白名单功能)
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断,因为前端模板采用了thymeleaf,这里不能直接使用 ("/static/**", "anon")来配置匿名访问,必须配置到每个静态目录
filterChainDefinitionMap.put("/login/*", "anon");//登陆页面
filterChainDefinitionMap.put("/restlogin/*", "anon");//基于Rest登陆
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/templates/**", "anon");
filterChainDefinitionMap.put("/log/initlog", "anon");
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* shiro 安全管理器(核心)-验证和授权都通过它
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(hxzRealm());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());//此处使用自定义sessionManager管理器,实现前后分离--如果不需要前后分离,可注释掉这行代码
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());//此处开启redis缓存管理--如果不需要redis缓存,可注释掉这行代码
return securityManager;
}
/**
* 密码加密配置
* @return
*/
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
// storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
@Bean
public HxzRealm hxzRealm() {
HxzRealm hxzRealm = new HxzRealm();
// 告诉realm,使用credentialsMatcher加密算法类来验证密文
hxzRealm.setCredentialsMatcher(hashedCredentialsMatcher());
hxzRealm.setCachingEnabled(false);
return hxzRealm;
}
//自定义sessionManager管理器,实现前后分离--如果不需要前后分离,可注释掉
@Bean
public SessionManager sessionManager() {
HxzSessionManager mySessionManager = new HxzSessionManager();
mySessionManager.setSessionDAO(redisSessionDAO());
return mySessionManager;
}
/**
* cacheManager 缓存 redis实现 ,如果不需要redis管理,可注释掉
* <p>
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现,通过自定义缓存管理器,如果不需要redis管理,可注释掉
* <p>
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* 配置shiro redisManager--自定义缓存管理器,实现redis进行缓存管理,如果不需要redis管理,可注释掉
* <p>
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);// 配置缓存过期时间,单位是秒,超过时间自动清空token
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
//---以下配置是加入注解,进行更细粒度权限控制
/**
* 配置Shiro生命周期处理器--此处使用了static 静态注入,否则@Value标签不生效
* @return
*/
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 下面的代码是添加注解支持
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
- 第六步:登陆验证
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/restlogin")
public class RestLoginController {
@RequestMapping(value = "/login", method = {RequestMethod.POST,RequestMethod.GET})
public String rsdome(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request) {
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return "未知账户";
} catch (IncorrectCredentialsException ice) {
return "密码不正确";
} catch (LockedAccountException lae) {
return "账户已锁定";
} catch (ExcessiveAttemptsException eae) {
return "用户名或密码错误次数过多";
} catch (AuthenticationException ae) {
return "用户名或密码不正确!";
}
if (subject.isAuthenticated()) {
return "登录成功,token="+ subject.getSession().getId();
} else {
token.clear();
return "登录失败";
}
}
}
- 第七步:怎么获取当前登陆用户
String username = (String) SecurityUtils.getSubject().getPrincipal();
好了,大体上实现无状态的服务集群+redis+shiro安全认证,既提升系统并发能力,又兼容PC和手机端的身份安全认证。这时小猿开心得头都秃了(事实上头早秃了!!)。小猿关上电脑,准备回家打王者。就在这时诡异的事情发生了,经理又来电了,说:“小猿啊,公司系统支步支持定时任务啊。。。。”