Shiro在单体应用中使用
shiro单Realm笔记
1、引入shiro依赖🎇
这里使用 ehcache 作为shiro的缓存使用,后续会有使用 Redis 作为缓存使用
<!--shiro依赖开始-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--在thymeleaf 使用shiro页面标签版本要对应-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--shiro依赖结束-->
2、配置 ShiroConfig 🎇
@Configuration
public class ShiroConfig {
/**
* shiroFilter相关配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager());
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 设置login URL
shiroFilterFactoryBean.setLoginUrl("/page/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/page/index");
// 未授权的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/page/nopermission");
// 静态资源
filterChainDefinitionMap.put("/static/**/**","anon");
// 登录
filterChainDefinitionMap.put("/login","anon");
// 注册
filterChainDefinitionMap.put("/reg_user","anon");
// 注册页面
filterChainDefinitionMap.put("/page/registered","anon");
filterChainDefinitionMap.put("/druid/**","anon");
/* 退出系统*/
filterChainDefinitionMap.put("/logout","logout");
/*现有资源的角色*/
filterChainDefinitionMap.put("/**","kickout,authc");
// 限制登录
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("kickout",kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 自定义Realm
*/
@Bean
public MyShiroRealm myShiroRealm() {
/*自定义realm*/
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
myShiroRealm.setCacheManager(ehCacheManager());
return myShiroRealm;
}
/**
* 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* 所以我们需要修改下doGetAuthenticationInfo中的代码; )
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于md5(md5(""));
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
/**
* 配置安全管理器 securityManager
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入自定义的realm;
securityManager.setRealm(myShiroRealm());
// 注入缓存管理器
securityManager.setCacheManager(ehCacheManager());
//注入记住我管理器
securityManager.setRememberMeManager(rememberMeManager());
// 注入session管理
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* shiro缓存管理器
*/
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache/ehcache.xml");
return cacheManager;
}
/**
* cookie 属性设置
*/
private SimpleCookie rememberMeCookie(){
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//如果httyOnly设置为true,则客户端不会暴露给客户端脚本代码,
// 使用HttpOnly cookie有助于减少某些类型的跨站点脚本攻击;
simpleCookie.setHttpOnly(true);
//<!-- 记住我cookie生效时间30天 ,单位秒;-->
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/** rememberMeManager管理器
* rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
*/
private CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
// 仅为测试使用,实际项目可更改为自定义生成,官方也是这么建议的(不记得在哪看到的)
cookieRememberMeManager.setCipherKey(Base64.decode("d3V0b25nAAAAAAAAAAAAAA=="));
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
/**
* 会话管理器
*/
@Bean
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 加入缓存管理器
sessionManager.setCacheManager(ehCacheManager());
// 删除过期的session
sessionManager.setDeleteInvalidSessions(true);
// 设置全局session超时时间
sessionManager.setGlobalSessionTimeout(1800000);
// 去掉JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
// 定义要使用的无效的Session定时调度器
sessionManager.setSessionValidationScheduler(scheduler());
// 是否定时检查session
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionDAO(sessionDAO());
return sessionManager;
}
/**
* 会话调度器
*/
@Bean
public ExecutorServiceSessionValidationScheduler scheduler(){
ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler();
scheduler.setInterval(1800000);
return scheduler;
}
@Bean
public EnterpriseCacheSessionDAO sessionDAO(){
EnterpriseCacheSessionDAO sessionDAO = new EnterpriseCacheSessionDAO();
sessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
return sessionDAO;
}
/**
* 同一个用户多设备登录限制
*/
public KickoutSessionControlFilter kickoutSessionControlFilter(){
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
kickoutSessionControlFilter.setCacheManager(ehCacheManager());
kickoutSessionControlFilter.setSessionManager(sessionManager());
// 同一个用户最大的会话数,默认-1无限制;比如2的意思是同一个用户允许最多同时两个人登录
kickoutSessionControlFilter.setMaxSession(1);
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序
kickoutSessionControlFilter.setKickoutAfter(false);
// 设置踢出后的地址,跳到登录界面
kickoutSessionControlFilter.setKickoutUrl("/page/login?kickout=1");
return kickoutSessionControlFilter;
}
/**
* 在thymeleaf 使用shiro页面标签
* */
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
/**
* 开启shiro aop注解支持 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
/**
* DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
}
3、自定义Realm🎇
/**
* @Author: LiJunYi
* @ClassName: MyShiroRealm
* @Description TODO:登录验证及授权实现
* @Version 1.0
*/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private IUserService userService;
/**
* 实现授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
Object principal = principals.getPrimaryPrincipal();
//2. 利用登录的用户的信息来查询当前用户的角色或权限列表
List<Role> roleList = userService.getRoles(principal.toString());
Set<String> roles = new HashSet<>();
Set<String> permissions = new HashSet<>();
for (Role role : roleList) {
roles.add(role.getRoleSymbol());
// 仅为测试,实际应该是数据库配置的信息
if ("admin".equals(role.getRoleSymbol())){
permissions.add("user:assign");
permissions.add("user:list");
}else{
permissions.add("user:list");
}
}
//3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
info.setStringPermissions(permissions);
//4. 返回 SimpleAuthorizationInfo 对象.
return info;
}
/**
* 实现登录验证
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1.把AuthenticationToken 转换为UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
// 2.从UsernamePasswordToken获取username
String username = upToken.getUsername();
// 3.调用数据库方法,从数据库获取username对应的用户记录
User user = userService.queryByUserName(username);
// 不存在该用户
if (null == user) {
throw new UnknownAccountException("无该用户");
}else if(1 !=user.getUserType()){
throw new LockedAccountException("账户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//以下信息是从数据库中获取的.
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
Object principal = username;
//2). credentials: 密码.
Object credentials = user.getUserPassword();
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值.
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}
4、单用户登录控制器🎇
/**
* @version 1.0.0
* @ClassName: KickoutSessionControlFilter
* @Description: 单用户登录控制(踢出前者)
* @author: LiJunYi
*/
public class KickoutSessionControlFilter extends AccessControlFilter {
/**
* 踢出后到的地址
*/
private String kickoutUrl;
/**
* 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
*/
private boolean kickoutAfter;
/**
* 同一个帐号最大会话数 默认1
*/
private int maxSession;
private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setCache(Cache<String, Deque<Serializable>> cache) {
this.cache = cache;
}
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-activeSessionCache");
}
/**
* 是否允许访问
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
/**
* 表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated() && !subject.isRemembered()) {
return true;
}
Session session = subject.getSession();
String userKey = ShiroUtils.getSysUser().getUserKey();
Serializable sessionId = session.getId();
// 初始化用户的队列放入缓存
Deque<Serializable> deque = cache.get(userKey);
if (deque == null) {
// 初始化队列
deque = new LinkedList<>();
}
// 如果队列没有此sessionID,且用户没有被踢出 放入队列
String kicKoutKey = "kickout";
if (!deque.contains(sessionId) && session.getAttribute(kicKoutKey) == null) {
// 将sessionId存入队列
deque.push(sessionId);
// 将用户的sessionId队列缓存
cache.put(userKey, deque);
}
//如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
Serializable kickoutSessionId = kickoutAfter ? deque.removeFirst() : deque.removeLast();
// 踢出后再更新下缓存队列
cache.put(userKey, deque);
try {
// 获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (null != kickoutSession) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute(kicKoutKey, true);
}
} catch (Exception e) {
// 面对异常,我们选择忽略
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute(kicKoutKey) != null && (boolean) session.getAttribute(kicKoutKey)){
// 会话被踢出
subject.logout();
saveRequest(request);
return isAjaxResponse(request,response);
}
return true;
}
private boolean isAjaxResponse(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if(WebUtilsPro.isAjaxRequest(req)){
// 输出JSON
Map<String,Object> map = new HashMap<>(8);
map.put("code", "501");
map.put("msg","系统提示:您已在别处登录,若不是您本人操作,请重新登录!");
WebUtilsPro.writeJson(map, res);
}else{
WebUtils.issueRedirect(request,response,kickoutUrl);
}
return false;
}
}
5、登录方法🎇
@Controller
public class LoginController {
@Autowired
private IUserService userService;
@Autowired
private IPermissionService permissionService;
@Autowired
HttpSession session;
/**
* 登录方法
*/
@RequestMapping("login")
@ResponseBody
public String login(User user, Boolean rememberMe){
// 处理结果
String reMsg = "";
// 获取当前登录用户
Subject currentUser = SecurityUtils.getSubject();
// 验证是否登录
if(!currentUser.isAuthenticated()){
// 用户名密码封装
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(),user.getUserPassword());
// 配置记住我
if(null == rememberMe){
rememberMe = false;
}
token.setRememberMe(rememberMe);
try {
// 执行登录
currentUser.login(token);
reMsg="登录成功";
// 进行菜单处理
User user1 = userService.queryByUserName(user.getUserName());
Permission root = permissionService.queryPermissionByUser(user1.getId());
session.setAttribute("rootPermission", root);
session.setAttribute("LoginUser", user1);
return reMsg;
}catch (IncorrectCredentialsException e){
System.out.println("密码错误"+e.getMessage());
reMsg="密码错误";
}catch (LockedAccountException e){
System.out.println("账号已被锁定");
reMsg="账号已被锁定";
}catch (DisabledAccountException e){
System.out.println("账号被禁用");
reMsg="账号被禁用";
}catch (UnknownAccountException e){
System.out.println("账号不存在");
reMsg="账号不存在";
}catch (UnauthorizedException e) {
System.out.println("没有权限进入");
reMsg="没有权限进入";
}catch (AuthenticationException ae){
System.out.println("用户名或密码错误");
reMsg = "用户名或密码错误";
}
return reMsg;
}
return reMsg;
}
}
个人看法😎
shiro作为一个老牌的安全框架,学习成本相对Security来讲还是容易的多。而且shiro在单体应用中,并且如果只需要一个Realm的话,依照笔记步骤来还是非常容易实现项目里安全认证及权限控制的。
实际项目上,可能会有多个Realm情况,比如只需要短信+用户名验证、验证码+用户名验证这些,像这些情况我们就需要写多个Realm去实现对应的逻辑代码。还有像可能在部署项目的时候,需要实现负载均衡功能,那就不能使用EhCache缓存,需要换成像Redis这种来实现session的共享,从而来实现负载均衡的效果。
上面提及的各种情况,博主后续会继续整理更新出来,如果有描述不对或者错误的地方,希望指正,共同进步!😁😁😁