请多多留言指教
什么是Shiro?
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
Shiro核心组件
Subject,SecurityManager,Realms.
1、Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
2、SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
3、Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
通俗说三者的关系:
Subject收集用户名密码,交给SecurityManager,SecurityManager负责调度Realms来匹配用户名密码,如果返回true则验证成功,否则验证失败。
Shiro的特性?
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。
Authentication(认证):用户身份识别,通常被称为用户“登录”。
Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
Session Management(会话管理):特定于用户的会话管理。
Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用
shiro介绍不多说了,直接上自己测试的案例代码!!!
注:
<a> 数据库:MySQL
<b> 相关表:用户表,角色表,权限表,用户角色表,角色权限表(用户、角色、权限等数据查询代码省略)
<c> CommonConstant.SHIRO_ENCRYPTION_NUMBER公共常量值1024,即密码加盐加密次数
1、pom.xml配置
注:相关springboot依赖省略!
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.2</version>
</dependency>
<!-- shiro ehcache缓存 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
<!-- shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
2、shiro config配置
package com.ayiol.business.config;
import com.ayiol.business.AyiolBackendApplication;
import com.ayiol.business.constant.CommonConstant;
import com.ayiol.business.shiro.MyShiroRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SecurityManager;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
/**
* shiro config
*
* @Author: LJG
* @Date: 2020-07-18
*/
@Configuration
public class ShiroConfig {
private static final Logger logger = LoggerFactory.getLogger(AyiolBackendApplication.class);
/**
* 密码校验规则HashedCredentialsMatcher
* 这个类是为了对密码进行编码的 ,
* 防止密码在数据库里明码保存 , 当然在登陆认证的时候 ,
* 这个类也负责对form里输入的密码进行编码
* 处理认证匹配处理器:如果自定义需要实现继承HashedCredentialsMatcher
*/
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//加密方式 散列算法:MD2、MD5、SHA-1、SHA-256、SHA-384、SHA-512等。
credentialsMatcher.setHashAlgorithmName("MD5");
//加密次数
credentialsMatcher.setHashIterations(CommonConstant.SHIRO_ENCRYPTION_NUMBER);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
/**
* 自定义身份认证 realm;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 MyShiroRealm,否则会影响 MyShiroRealm类 中其他类的依赖注入
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
/**
* 注入 securityManager
* 权限管理,配置主要是Realm的管理认证
*/
@Bean("securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
*
* @param securityManager securityManager
* @return ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
logger.info("======>load shiro config");
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置securityManager
bean.setSecurityManager(securityManager);
//设置登录页面
bean.setLoginUrl("/login");
// //设置登录成功跳转的页面
// bean.setSuccessUrl("/pages/index.html");
// //设置未授权跳转的页面
// bean.setUnauthorizedUrl("/pages/unauthorized.html");
//定义过滤器
LinkedHashMap<String, String> filterMapper = new LinkedHashMap<>();
/**
* 放行静态资源
*/
filterMapper.put("/css/**", "anon");
filterMapper.put("/images/**", "anon");
filterMapper.put("/js/**", "anon");
/**
* 放行公共界面
*/
filterMapper.put("/login", "anon");
filterMapper.put("/logout", "anon");
//需要登录访问的资源 , 一般将/**放在最下边
filterMapper.put("/**", "authc");
bean.setFilterChainDefinitionMap(filterMapper);
return bean;
}
/**
* 开启aop注解支持
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public RememberMeManager rememberMeManager() {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
//注入自定义cookie(主要是设置寿命, 默认的一年太长)
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setHttpOnly(true);
//设置RememberMe的cookie有效期
simpleCookie.setMaxAge(CommonConstant.SHIRO_COOKIE_EXPIRE_TIME);
rememberMeManager.setCookie(simpleCookie);
//手动设置对称加密秘钥,防止重启系统后系统生成新的随机秘钥,防止导致客户端cookie无效
rememberMeManager.setCipherKey(Base64.decode("YV95aW9fbDIwXzIwc2VfcnZlcg=="));
return rememberMeManager;
}
@Bean
public EhCacheManager getCache() {
return new EhCacheManager();
}
}
3、MyShiroRealm自定义Beam
package com.ayiol.business.shiro;
import com.ayiol.business.AyiolBackendApplication;
import com.ayiol.business.Utils.RedisUtil;
import com.ayiol.business.Utils.SpringBeanFactoryUtil;
import com.ayiol.business.constant.CommonConstant;
import com.ayiol.business.entity.User;
import com.ayiol.business.entity.UserRole;
import com.ayiol.business.service.RoleService;
import com.ayiol.business.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* shiro realm
*
* @Author: LJG
* @Date: 2020-07-08 16:00
*/
public class MyShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(AyiolBackendApplication.class);
@Autowired
private UserService userService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private RoleService roleService;
/**
* 授权
*
* @param principalCollection 主要的控制器
* @return Authorization
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("=========> 进入自定义权限设置方法!");
if (null == userService) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
if (null == roleService) {
roleService = (RoleService) SpringBeanFactoryUtil.getBeanByName("roleServiceImpl");
}
// 1 获取用户信息
User user = (User) principalCollection.getPrimaryPrincipal();
if (null == user) {
return null;
}
// 2 获取用户的角色id
List<UserRole> userRoles = userService.getAuthUserRole(null == user ? "" : user.getId());
List<String> roleIds = userRoles.stream().filter(f -> null != f.getRole()).map(ur -> ur.getRole().getId()).collect(Collectors.toList());
List<String> roleCodes = userRoles.stream().filter(f -> null != f.getRole()).map(ur -> ur.getRole().getCode()).collect(Collectors.toList());
Set<String> roleCodesSet = new HashSet<>(roleCodes);// 转换为Set类型
// 3 获取用户的权限id
List<String> permissionNames = roleService.getAuthPermission(roleIds);
Set<String> permissionNamesSet = new HashSet<>(permissionNames);// 转换为Set类型
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 4 设置权限
simpleAuthorizationInfo.setStringPermissions(permissionNamesSet);
// 5 设置角色
simpleAuthorizationInfo.setRoles(roleCodesSet);
return simpleAuthorizationInfo;
}
/**
* 身份认证
*
* @param token 认证token
* @return Authentication
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
logger.info("=========> 进入自定义登录认证方法!");
//表示没有登录
//加这一步的目的是在Post请求的时候会先进认证,然后在到请求
if (null == token.getPrincipal()) {
return null;
}
// 如果userService为空时,则手动加载userService
if (null == userService) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
String pwd = String.valueOf(usernamePasswordToken.getPassword());
// 通过username从数据库中查找 User对象
User user = userService.queryByUsername(username);
if (null == user) {
throw new UnknownAccountException(); // 用户不存在
}
// ------------------------------- 认证方式:SimpleAuthenticationInfo加盐 -----------------------------
// 给登录用户名、密码加盐,按特定规则生成一个新的密码;赋值给usernamePasswordToken用于跟SimpleAuthenticationInfo校验密码是否一致
String saltPwd = createSalting(username, pwd);
usernamePasswordToken.setPassword(saltPwd.toCharArray());
//根据用户的情况,来构建AuthenticationInfo对象,通常使用的实现类为SimpleAuthenticationInfo
//以下信息是从数据库中获取的
//1)principal:认证的实体信息,可以是username,也可以是数据库表对应的用户的实体对象
Object principal = user.getUsername();
//2)credentials:密码
Object credentials = user.getPassword();
//3)realmName:当前realm对象的name,调用父类的getName()方法即可
String realmName = getName();
//4)credentialsSalt盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(principal);//使用账号作为盐值
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, credentials, credentialsSalt, realmName);
return info;
}
/**
* 密码加盐
*
* @param username 用户名
* @param crdentials 密码原值
* @return 加盐后的密码
*/
private static String createSalting(String username, String crdentials) {
String hashAlgorithmName = "MD5";//加密方式
ByteSource salt = ByteSource.Util.bytes(username);//以用户名为盐值
int hashIterations = CommonConstant.SHIRO_ENCRYPTION_NUMBER;//加密次数
Object result = new SimpleHash(hashAlgorithmName, crdentials, salt, hashIterations);
return result + "";
}
}
4、LoginController.java /login登录路由
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Map<String, Object> login(@RequestBody Map paramMap) {
// 获取参数 校验
String username = StringUtils.isEmpty(paramMap.get("username")) ? "" : paramMap.get("username").toString();
String password = StringUtils.isEmpty(paramMap.get("password")) ? "" : paramMap.get("password").toString();
if ("".equals(username)) {
return ResultUtil.badRequest("username", paramMap.get("username"), "用户名不能为空");
}
if ("".equals(password)) {
return ResultUtil.badRequest("password", paramMap.get("password"), "密码不能为空");
}
// //如果用户已登录,先踢出
// ShiroSecurityHelper.kickOutUser(user.getUsername());
/**
* 使用shiro认证
*/
//1,获取subject
Subject subject = SecurityUtils.getSubject();
//2,封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if (!StringUtils.isEmpty(paramMap.get("rememberMe"))
&& CommonConstant.ONE_STRING.equals(paramMap.get("rememberMe").toString())) {
token.setRememberMe(true);
}
//3,执行登录方法
try {
subject.login(token);
} catch (UnknownAccountException uae) {
// 用户名未知
return ResultUtil.failureResponse("用户不存在!");
} catch (IncorrectCredentialsException ice) {
// 凭据不正确,例如密码不正确
return ResultUtil.failureResponse("密码错误!");
} catch (LockedAccountException lae) {
// 用户被锁定,例如管理员把某个用户禁用
return ResultUtil.failureResponse("用户被锁定!");
} catch (ExcessiveAttemptsException eae) {
// 尝试认证次数多余系统指定次数
return ResultUtil.failureResponse("尝试认证次数过多,请稍后重试!");
} catch (AuthenticationException ae) {
// 其他未指定异常
return ResultUtil.failureResponse("未知异常!");
}
// 从shiro中获取用户信息
User user = (User) SecurityUtils.getSubject().getPrincipal();
user.setPassword(null);
return ResultUtil.ok(user);
}
5、LoginController.java /logout登出路由
@RequestMapping(value = "/logout", method = RequestMethod.POST)
public Map<String, Object> logout(@RequestBody Map paramMap) {
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
User user = (User) subject.getPrincipal();// 获取登录用户信息
subject.logout();
}
return ResultUtil.ok("登出成功!");
}
6、项目为前后端分离,这里使用的注解方式测试认证、授权
import com.alibaba.fastjson.JSON;
@RequiresRoles(value = {"admin"})
@RequiresPermissions(value = {"all", "read"}, logical = Logical.OR)
@PostMapping("")
public Map<String, Object> save(@RequestBody Map paramMap) {
if (StrUtil.isEmptyIfStr(paramMap.get("username"))) {
return ResultUtil.failureResponse("登录用户名不能为空");
}
User exsistUser = userService.queryByUsername(paramMap.get("username").toString());
if (null != exsistUser) {
return ResultUtil.failureResponse("登录用户名已经存在");
}
//转换为用户实体类
User user = JSON.parseObject(JSON.toJSONString(paramMap), User.class);
user.setId(UUID.randomUUID().toString());
user.setCreatedAt(new Date());
userService.save(user);
user.setPassword(null);
return ResultUtil.ok(user);
}
到此就完成了!有什么问题可以留言。