认证
,授权
,加密
,会话管理
,Web集成
,缓存
等。Authentication
:身份认证、登录,验证用户是不是拥有相应的身份;Authorization
:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否Session Manager
:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都Cryptography
:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;Web Support
:Web支持,可以非常容易的集成到Web环境;Caching
:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高Concurrency
:Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限Testing
:提供测试支持;Run As
:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;Remember Me
:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了从外部来看shiro,即从应用程序角度来观察如何使用shiro完成工作:
Subject
:代表当前"用户"。与当前应用程序交互的任何东西都是Subject,如爬虫、机器人。所有的Subject 都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager。Subject 是一个门面,SecurityManager 是实际的执行者。SecurityManager
:与安全有关的操作都会与SecurityManager交互。它管理者所有Subject,是Shiro的核心,负责与其他组件进行交互。Realm
: Shiro从Realm中获取安全数据(用户、角色、权限)。SecurityManager需要从Realm中获取响应的用户信息进行比较用户身份是否合法,也需要从Realm中得到用户相应的角色/权限进行验证,以确定用户是否能够操作。Subject
:任何可以与应用交互的 用户
;Security Manager
:相当于SpringMVC中的DispatcherServlet;是Shiro的心脏,所有具体的交互Authenticator
:负责Subject认证,是一个扩展点,可以自定义实现;可以使用认证策略Authorizer
:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能Realm
:可以有一个或者多个的realm,可以认为是安全实体数据源,即用于获取安全实体的,可SessionManager
:管理Session生命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用CacheManager
:缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改Cryptography
:密码模块,Shiro 提供了一些常见的加密组件用于密码加密,解密等流程如下:
- 1、首先调用 Subject.isPermitted*/hasRole*接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
- 2、Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:view”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
- 3、在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
- 4、Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。
ModularRealmAuthorizer 进行多 Realm 匹配流程:
- 首先检查相应的 Realm 是否实现了实现了 Authorizer;
- 如果实现了 Authorizer,那么接着调用其相应的 isPermitted*/hasRole* 接口进行匹配;
- 如果有一个 Realm 匹配那么将返回 true,否则返回 false。
如果 Realm 进行授权的话,应该继承 AuthorizingRealm,其流程是:
- 如果调用 hasRole*,则直接获取 AuthorizationInfo.getRoles() 与传入的角色比较即可;首先如果调用如 isPermitted(“user:view”),首先通过 PermissionResolver 将权限字符串转换成相应的 Permission 实例,默认使用 WildcardPermissionResolver,即转换为通配符的 WildcardPermission;
- 通过 AuthorizationInfo.getObjectPermissions() 得到 Permission 实例集合;通过 AuthorizationInfo.getStringPermissions() 得到字符串集合并通过 PermissionResolver 解析为 Permission 实例;然后获取用户的角色,并通过 RolePermissionResolver 解析角色对应的权限集合(默认没有实现,可以自己提供);
- 接着调用 Permission.implies(Permission p) 逐个与传入的权限比较,如果有匹配的则返回 true,否则 false。
Shiro
从 Realm
获取安全数据(如用户、角色、权限),就是说 SecurityManager
要验证用户身份,那么它需要从 Realm
获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm
得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm
看成 DataSource
String getName(); //返回一个唯一的Realm名字
boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException; //根据Token获取认证信息
public class MyRealm implements Realm {
@Override
public String getName() {
return "myrealm";
}
@Override
public boolean supports(AuthenticationToken token) {
//仅支持UsernamePasswordToken类型的Token
return token instanceof UsernamePasswordToken;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal(); //得到用户名
String password = new String((char[])token.getCredentials()); //得到密码
if(!"zhang".equals(username)) {
throw new UnknownAccountException(); //如果用户名错误
}
if(!"123".equals(password)) {
throw new IncorrectCredentialsException(); //如果密码错误
}
//如果身份认证验证成功,返回一个AuthenticationInfo实现;
return new SimpleAuthenticationInfo(username, password, getName());
}
}
ini 配置文件类似于 Java 中的 properties(key=value),不过提供了将 key/value 分类的特性,key 是每个部分不重复即可,而不是整个配置文件。如下是 INI 配置分类:
[main]
\#提供了对根对象securityManager及其依赖的配置
securityManager=org.apache.shiro.mgt.DefaultSecurityManager
…………
securityManager.realms=$jdbcRealm
[users]
\#提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2
username=password,role1,role2
[roles]
\#提供了角色及权限之间关系的配置,角色=权限1,权限2
role1=permission1,permission2
[urls]
\#用于web,提供了对web url拦截相关的配置,url=拦截器[参数],拦截器
/index.html = anon
/admin/** = authc, roles[admin], perms["permission1"]
[main]
部分提供了对根对象 securityManager 及其依赖对象(Realm、authenticator、authcStrategy)的配置。
#声明一个realm
MyRealm1=com.shiro.mutilrealm.MyRealm1
MyRealm2=com.shiro.mutilrealm.MyRealm2
#配置验证器
authenticator = org.apache.shiro.authc.pam.ModularRealmAuthenticator
# AllSuccessfulStrategy 表示 MyRealm1和MyRealm2 认证都通过才算通过
#配置策略
#authcStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy
authcStrategy = com.shiro.authenticationstrategy.MyAuthenticationStrategy
#将验证器和策略关联起来
authenticator.authenticationStrategy = $authcStrategy
#配置验证器所使用的Realm
authenticator.realms=$MyRealm2,$MyRealm1
#把Authenticator设置给securityManager
securityManager.authenticator = $authenticator
[users]
部分配置用户名 / 密码及其角色,格式:“用户名 = 密码,角色 1,角色 2”,角色部分可省略。
[users]
zhang=123,role1,role2
wang=123
[roles]
部分配置角色及权限之间的关系,格式:“角色 = 权限 1,权限 2”;
[roles]
role1=user:create,user:update
role2=*
这里涉及到密码,就牵扯到加密的问题,我们可以MD5,Sha1,Sha256等算法进行加密
[main]
#告诉shiro我们用哪个加密算法
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
iniRealm.credentialsMatcher = $sha256Matcher
[users]
#用户名=密码,角色
admin=355b1bbfc96725cdce8f4a2708fda310a80e6d13315aec4e5eed2a75fe8032ce,role1
获取密码的 hex 加密字符串
String ss = new Sha256Hash(“cc”).toHex();
[urls]
部分配置 url 及相应的拦截器之间的关系,格式:“url = 拦截器 [参数],拦截器 [参数]
/login=anon
/unauthorized=anon
/static/**=anon
/authenticated=authc
/role=authc,roles[admin]
/permission=authc,perms["user:create"]
anon: 表示匿名访问,即不需要登录就可以访问,例如/login这个路径可以匿名访问
authc: 表示需要登录才能访问
roles[admin]: 表示有admin这个角色的用户才能访问
perms[“user:create”]: 表示有这个权限的才能访问
?:匹配一个字符
*:匹配零个或多个字符
**:匹配零个或多个路径
String str = "lyp";
String salt = "123";
String md5 = new Md5Hash(str, salt).toString();//还可以转换为 toBase64()/toHex()
如上代码通过盐 “123” MD5散列 “lyp”,另外散列时还可以指定散列次数。
如2次表示:
md5(md5(str)):“new Md5Hash(str, salt, 2).toString()
String str = "lyp";
String salt = "123";
String sha1 = new Sha256Hash(str, salt).toString();
String str = "lyp";
String salt = "123";
//内部使用MessageDigest
String simpleHash = new SimpleHash("SHA-1", str, salt).toString();
为了方便使用,Shiro 提供了 HashService,默认提供了 DefaultHashService 实现。
DefaultHashService hashService = new DefaultHashService(); //默认算法SHA-512
hashService.setHashAlgorithmName("SHA-512");
hashService.setPrivateSalt(new SimpleByteSource("123")); //私盐,默认无
hashService.setGeneratePublicSalt(true);//是否生成公盐,默认false
hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());//用于生成公盐。默认就这个
hashService.setHashIterations(1); //生成Hash值的迭代次数
HashRequest request = new HashRequest.Builder()
.setAlgorithmName("MD5").setSource(ByteSource.Util.bytes("hello"))
.setSalt(ByteSource.Util.bytes("123")).setIterations(2).build();
String hex = hashService.computeHash(request).toHex();
- 首先创建一个 DefaultHashService,默认使用 SHA-512 算法;
- 以通过 hashAlgorithmName 属性修改算法;
- 可以通过 privateSalt 设置一个私盐,其在散列时自动与用户传入的公盐混合产生一个新盐;
- 可以通过 generatePublicSalt 属性在用户没有传入公盐的情况下是否生成公盐;
- 可以设置 randomNumberGenerator 用于生成公盐;
- 可以设置 hashIterations 属性来修改默认加密迭代次数;
- 需要构建一个 HashRequest,传入算法、数据、公盐、迭代次数。
SecureRandomNumberGenerator 用于生成一个随机数:
SecureRandomNumberGenerator randomNumberGenerator =
new SecureRandomNumberGenerator();
randomNumberGenerator.setSeed("123".getBytes());
String hex = randomNumberGenerator.nextBytes().toHex();
DefaultPasswordService 配合 PasswordMatcher 实现简单的密码加密与验证服务
1、定义Realm
public class MyRealm extends AuthorizingRealm {
private PasswordService passwordService;
public void setPasswordService(PasswordService passwordService) {
this.passwordService = passwordService;
}
//省略doGetAuthorizationInfo,具体看代码
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo( "wu",passwordService.encryptPassword("123"),getName());
}
}
为了方便,直接注入一个 passwordService 来加密密码,实际使用时需要在 Service 层使用 passwordService 加密密码并存到数据库。
2、ini 配置(shiro-passwordservice.ini)
[main]
passwordService=org.apache.shiro.authc.credential.DefaultPasswordService
hashService=org.apache.shiro.crypto.hash.DefaultHashService
passwordService.hashService=$hashService
hashFormat=org.apache.shiro.crypto.hash.format.Shiro1CryptFormat
passwordService.hashFormat=$hashFormat
hashFormatFactory=org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory
passwordService.hashFormatFactory=$hashFormatFactory
passwordMatcher=org.apache.shiro.authc.credential.PasswordMatcher
passwordMatcher.passwordService=$passwordService
myRealm=com.github.zhangkaitao.shiro.chapter5.hash.realm.MyRealm
myRealm.passwordService=$passwordService
myRealm.credentialsMatcher=$passwordMatcher
securityManager.realms=$myRealm
- passwordService 使用 DefaultPasswordService,如果有必要也可以自定义;
- hashService 定义散列密码使用的 HashService,默认使用 DefaultHashService(默认 SHA-256 算法);
- hashFormat 用于对散列出的值进行格式化,默认使用 Shiro1CryptFormat,另外提供了 Base64Format 和 HexFormat,对于>+ 有 salt 的密码请自定义实现 ParsableHashFormat 然后把 salt 格式化到散列值中;
- hashFormatFactory 用于根据散列值得到散列的密码和 salt;因为如果使用如 SHA 算法,那么会生成一个 salt,此 salt 需要>+ 保存到散列后的值中以便之后与传入的密码比较时使用;默认使用 DefaultHashFormatFactory;
- passwordMatcher 使用 PasswordMatcher,其是一个 CredentialsMatcher 实现;
- 将 credentialsMatcher 赋值给 myRealm,myRealm 间接继承了 AuthenticatingRealm,其在调用 getAuthenticationInfo 方法获取到 AuthenticationInfo 信息后,会使用 credentialsMatcher 来验证凭据是否匹配,如果不匹配将抛出 IncorrectCredentialsException 异常。
如上方式的缺点是:salt 保存在散列值中;没有实现如密码重试次数限制。
HashedCredentialsMatcher 实现密码验证服务
- Shiro 提供了 CredentialsMatcher 的散列实现 HashedCredentialsMatcher,和之前的 PasswordMatcher 不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己写,因为能提供自己的盐。
1、生成密码散列值
此处我们使用 MD5 算法,“密码 + 盐(用户名 + 随机数)” 的方式生成散列值:
String algorithmName = "md5";
String username = "lyp";
String password = "123";
String salt1 = username;
String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex();
int hashIterations = 2;
SimpleHash hash = new SimpleHash(algorithmName, password, salt1 + salt2, hashIterations);
String encodedPassword = hash.toHex();
如果要写用户模块,需要在新增用户 / 重置密码时使用如上算法保存密码,将生成的密码及 salt2 存入数据库(因为我们的散列算法是:md5(md5(密码 +username+salt2)))。
2、生成 Realm
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = "lyp"; //用户名及salt1
String password = "202cb962ac59075b964b07152d234b70"; //加密后的密码
String salt2 = "202cb962ac59075b964b07152d234b70";
SimpleAuthenticationInfo ai = new SimpleAuthenticationInfo(username, password, getName());
ai.setCredentialsSalt(ByteSource.Util.bytes(username+salt2)); //盐是用户名+随机数
return ai;
}
3、ini 配置
[main]
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true
myRealm=com.github.zhangkaitao.shiro.chapter5.hash.realm.MyRealm2
myRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$myRealm
- 通过 credentialsMatcher.hashAlgorithmName=md5 指定散列算法为 md5,需要和生成密码时的一样;
- credentialsMatcher.hashIterations=2,散列迭代次数,需要和生成密码时的意义;
- credentialsMatcher.storedCredentialsHexEncoded=true 表示是否存储散列后的密码为 16 进制,需要和生成密码时的一样,默认是 base64;
此处最需要注意的就是 HashedCredentialsMatcher 的算法需要和生成密码时的算法一样。另外 HashedCredentialsMatcher 会自动根据 AuthenticationInfo 的类型是否是 SaltedAuthenticationInfo 来获取 credentialsSalt 盐。
需要确保存入数据库的密码与如下的加密方式要一直,如md5加密,盐值为’lyp’ ,散列次数2
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
// 往数据库中查询用户
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getUsername, username));
if (user == null) {
// 账号不存在
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(
user, //用户
user.getPassword(), //密码
ByteSource.Util.bytes("lyp"), // 加密传入盐值
getName());//Realm name
}
/**
* shiro配置类
*
*/
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 登录配置
shiroFilter.setLoginUrl("/login");
shiroFilter.setSuccessUrl("/");
shiroFilter.setUnauthorizedUrl("/error/403");
// 自定义过滤器
Map<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("mlfc", new MyLoginFilter());
shiroFilter.setFilters(filtersMap);
// 拦截配置
Map<String, String> filterChainDefinitions = new LinkedHashMap<>();
filterChainDefinitions.put("/assets/**", "anon");
filterChainDefinitions.put("/login", "anon");
filterChainDefinitions.put("/reg", "anon");
filterChainDefinitions.put("/logout", "logout");
filterChainDefinitions.put("/**", "mlfc,user");
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitions);
return shiroFilter;
}
@Bean(name = "userRealm")
@DependsOn("lifecycleBeanPostProcessor")
public MyRealm userRealm() {
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(credentialsMatcher()); // 自定义加密规则
return myRealm;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());//将Realm注入到SecurityManager中。
return securityManager;
}
//因为我们的密码是加过密的,所以,如果要Shiro验证用户身份的话,需要告诉它我们用的是md5加密的,并且是加密了两次。同时我们在自己的Realm中也通过SimpleAuthenticationInfo返回了加密时使用的盐。这样Shiro就能顺利的解密密码并验证用户名和密码是否正确了。
@Bean(name = "credentialsMatcher)"
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
}
shiro 登录核心
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//执行登录方法,如果没有异常就说明OK了
subject.login(token);
/**
* 登录
*/
@PostMapping("/login")
@ResponseBody
public R login(HttpServletRequest request, String username, String password, String code, boolean rememberMe) {
if (StringUtil.isBlank(username, password)) {
return R.failed("账号或密码不能为空");
}
String sessionCode = (String) request.getSession().getAttribute("captcha");
if (code == null || !sessionCode.equals(code.trim().toLowerCase())) {
return R.failed("验证码不正确");
}
try {
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
SecurityUtils.getSubject().login(token);
return R.succeed("登录成功");
} catch (UnknownAccountException e) {
return R.failed("用户不存在");
} catch (IncorrectCredentialsException e) {
return R.failed("密码错误");
} catch (ExcessiveAttemptsException eae) {
return R.failed("操作频繁,请稍后再试");
}
}
//永不过期,在登陆最开始加上
SecurityUtils.getSubject().getSession().setTimeout(-1000L);
//其他时间 单位毫秒
SecurityUtils.getSubject().getSession().setTimeout(1800000);
Filter名称 | 说明 | |
---|---|---|
anon | 无参,匿名访问,无需认证就可访问 | 无参,匿名访问,无需认证就可访问 |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 无参,表示需要认证才能访问 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | 无参,表示需要httpBasic认证才能访问 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出拦截器,即退出后重定向的地址 |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常; |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 权限授权拦截器,验证用户是否拥有所有权限 |
port | org.apache.shiro.web.filter.authz.PortFilter | 端口拦截器,端口号不是指定端口号,则跳转过去 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | rest风格拦截器,根据请求方式来识别 |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 拥有某个角色权限才能访问 |
ssl | org.apache.shiro.web.filter.authz.SslFilter | 无参,SSL拦截器,只有请求协议是https才能通过 |
user | org.apache.shiro.web.filter.authc.UserFilter | 无参,用户已经身份验证,或记住我登录可以访问 |
/admin/**=anon
:无参,表示可匿名访问/admin/user/**=authc
:无参,表示需要认证才能访问/admin/user/**=authcBasic
:无参,表示需要httpBasic认证才能访问/admin/user/**=ssl
:无参,表示需要安全的URL请求,协议为https/home=user
:表示用户不一定需要通过认证,只要曾被 Shiro 记住过登录状态就可以正常发起 /home 请求/edit=authc,perms[admin:edit]
:表示用户必需已通过认证,并拥有 admin:edit 权限才可以正常发起 /edit 请求/admin=authc,roles[admin]
:表示用户必需已通过认证,并拥有 admin 角色才可以正常发起 /admin 请求/admin/user/**=port[8081]
:当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString/admin/user/**=rest[user]
:根据请求方式来识别,相当于 /admins/user/**=perms[user:get]或perms[user:post]
等等/admin**=roles["admin,guest"]
:允许多个参数(逗号分隔),此时要全部通过才算通过,相当于hasAllRoles()/admin**=perms["user:add:*,user:del:*"]
:允许多个参数(逗号分隔),此时要全部通过才算通过,相当于isPermitedAll()一般使用@RequiresPermissions即可
RequiresAuthentication:
使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。
RequiresGuest:
使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。
RequiresPermissions:
当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
RequiresRoles:
当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
RequiresUser
当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
<%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro">
1.shiro:authenticated (表示已认证通过,但不包括remember me登录的)
<shiro:authenticated>
<label>用户身份验证已通过 label>
shiro:authenticated>
说明:只有已通过用户认证,但不是通过记住我(remember me)浏览才会看到标签内的内容
2.shiro:guest (表示是游客身份,没有登录)
<shiro:guest>
<label>您当前是游客,label><a href="/login.jsp" >请登录a>
shiro:guest>
说明:只有是没有登录过,以游客的身份浏览才会看到标签内的内容
3.shiro:hasAnyRoles(表示拥有这些角色中其中一个)
<shiro:hasAnyRoles name="admin,user">
<label>这是拥有admin或者是user角色的用户label>
shiro:hasAnyRoles>
说明:只有成功登录后,且具有admin或者user角色的用户才会看到标签内的内容;name属性中可以填写多个角色名称,以逗号(,)分隔
4.shiro:hasPermission(表示拥有某一权限)
<shiro:hasPermission name="admin:add">
<label>这个用户拥有admin:add的权限label>
shiro:hasPermission>
说明:只有成功登录后,且具有admin:add权限的用户才可以看到标签内的内容,name属性中只能填写一个权限的名称
5.shiro:hashRole (表示拥有某一角色)
<shiro:hasRole name="admin">
<label>这个用户拥有的角色是adminlabel>
shiro:hasRole>
说明:只有成功登录后,且具有admin角色的用户才可以看到标签内的内容,name属性中只能填写一个角色的名称
6.shiro:lacksPermission (表示不拥有某一权限)
<shiro:lacksPermission name="admin:delete">
<label>这个用户不拥有admin:delete的权限label>
shiro:lacksPermission>
说明:只有成功登录后,且不具有admin:delete权限的用户才可以看到标签内的内容,name属性中只能填写一个权限的名称
7.shiro:lacksRole (表示不拥有某一角色)
<shiro:lacksRole name="admin">
<label>这个用户不拥有admin的角色label>
shiro:lacksRole>
说明:只有成功登录后,且不具有admin角色的用户才可以看到标签内的内容,name属性中只能填写一个角色的名称
8.shiro:notAuthenticated (表示没有通过验证)
<shiro:notAuthenticated>
<label>用户身份验证没有通过(包括通过记住我(remember me)登录的) label>
shiro:notAuthenticated>
说明:只有没有通过验证的才可以看到标签内的内容,包括通过记住我(remember me)登录的
9.shiro:principal (表示用户的身份)
....
return new SimpleAuthenticationInfo(user,user.getPswd(), getName());
<shiro: principal/>
<shiro:principal property="username"/>
10.shiro:user (表示已登录)
<shiro:user>
<label>欢迎[<shiro:principal/>],label><a href="/logout.jsp">退出a>
shiro:user>
说明:只有已经登录(包含通过记住我(remember me)登录的)的用户才可以看到标签内的内容;一般和标签shiro:principal一起用,来做显示用户的名称
注意:
shiro的jsp标签可以嵌套使用,可以根据业务的具体场景进行使用。例如一个按钮需要排除不是admin或user角色的用户才可以显示,可以像如下这样实现:
<shiro:lacksRole name="admin">
<shiro:lacksRole name="user">
<label>这个用户不拥有admin或user的角色label>
shiro:lacksRole>
shiro:lacksRole>
用户 | 角色 | 权限 |
---|---|---|
svip | svip | [查看、更新、删除、新增] |
vip | vip | [查看、更新、新增] |
user | p | [查看] |
/**
* ShiroConfig配置
*/
@Configuration
public class ShiroConfig {
//SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
@Bean("securityManager")
public SecurityManager securityManager(AuthRealm authRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authRealm); //将Realm注入到SecurityManager中。
return securityManager;
}
/**
* anno: 无需认证就可以访问
* authc: 必须认证才能访问
* user: 必须拥有 记住我 功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
*/
/**
* shiro如果使用ShiroConfig中shiroFiltet的map进行权限或角色拦截,会出现只走登陆认证,不走授权认证的情况。这是个巨坑!
//主要是这部分: 不要用这种方法,最好用注解的方法
filterMap.put("/add", "roles[admin]");
filterMap.put("/list", "roles[admin,user]");
filterMap.put("/delete", "perms[admin:delete]");
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//auth过滤
Map<String, Filter> filters = new HashMap<>();
filters.put("auth", new AuthFilter());
shiroFilter.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
//authc表示需要验证身份才能访问,还有一些比如anon表示不需要验证身份就能访问等。
// anno匿名访问 auth验证
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/doc.html", "anon");
// 除了以上路径,其他都需要权限验证
filterMap.put("/**", "auth");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
//负责管理shiro的生命周期,项目中不加lifecyclebeanpostprocessor也没问题
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
@Component
public class AuthRealm extends AuthorizingRealm {
@Autowired
private ShiroService shiroService;
private String salt = "lyp";
@Override
/**
* 授权 获取用户的角色和权限
*@param [principals]
*@return org.apache.shiro.authz.AuthorizationInfo
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
User user = (User) principals.getPrimaryPrincipal();
//Integer userId = user.getUserId();
//2.添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//2.1添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
for (Permission permission : role.getPermissions()) {
//2.1.1添加权限
simpleAuthorizationInfo.addStringPermission(permission.getPermission());
}
}
return simpleAuthorizationInfo;
}
@Override
/**
* 认证 判断token的有效性
*@param [token]
*@return org.apache.shiro.authc.AuthenticationInfo
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取token,既前端传入的token
String accessToken = (String) token.getPrincipal();
//1. 根据accessToken,查询用户信息
SysToken tokenEntity = shiroService.findByToken(accessToken);
//2. token失效
if (tokenEntity == null || tokenEntity.getExpireTime().isBefore(LocalDateTime.now())) {
throw new IncorrectCredentialsException("token失效,请重新登录");
}
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
User user = shiroService.findByUserId(tokenEntity.getUserId());
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
user, //用户
accessToken,
this.getName()); //Realm name
return info;
}
}
@Api(tags = "测试")
@RestController
public class TestController {
@RequiresPermissions({"save"})
@PostMapping("/save")
public Map<String, Object> save(@RequestHeader("token")String token) {
System.out.println("save");
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", 200);
map.put("msg", "当前用户有save的权力");
return map;
}
@RequiresPermissions({"delete"})
@DeleteMapping("/delete")
public Map<String, Object> delete(@RequestHeader("token")String token) {
System.out.println("delete");
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", 200);
map.put("msg", "当前用户有delete的权力");
return map;
}
@RequiresPermissions({"update"})
@PutMapping("update")
public Map<String, Object> update(@RequestHeader("token")String token) {
System.out.println("update");
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", 200);
map.put("msg", "当前用户有update的权力");
return map;
}
@RequiresPermissions({"select"})
@GetMapping("select")
public Map<String, Object> select(@RequestHeader("token")String token) {
System.out.println("select");
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", 200);
map.put("msg", "当前用户有select的权力");
return map;
}
@RequiresRoles({"vip"})
@GetMapping("/vip")
public Map<String, Object> vip(@RequestHeader("token")String token) {
System.out.println("vip");
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", 200);
map.put("msg", "当前用户有VIP角色");
return map;
}
@RequiresRoles({"svip"})
@GetMapping("/svip")
public Map<String, Object> svip(@RequestHeader("token")String token) {
System.out.println("svip");
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", 200);
map.put("msg", "当前用户有SVIP角色");
return map;
}
@RequiresRoles({"p"})
@GetMapping("/p")
public Map<String, Object> p(@RequestHeader("token")String token) {
System.out.println("p");
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", 200);
map.put("msg", "当前用户有P角色");
return map;
}
}
@Api(tags = "Shiro权限管理")
@RestController
public class ShiroController {
private final ShiroService shiroService;
public ShiroController(ShiroService shiroService) {
this.shiroService = shiroService;
}
/**
* 登录
*/
@ApiOperation(value = "登陆", notes = "参数:用户名 密码")
@PostMapping("/sys/login")
public Map<String, Object> login(@RequestBody @Validated LoginDTO loginDTO, BindingResult bindingResult) {
Map<String, Object> result = new HashMap<>();
if (bindingResult.hasErrors()) {
result.put("status", 400);
result.put("msg", bindingResult.getFieldError().getDefaultMessage());
return result;
}
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
//用户信息
User user = shiroService.findByUsername(username);
//账号不存在、密码错误
if (user == null || !user.getPassword().equals(password)) {
result.put("status", 400);
result.put("msg", "账号或密码有误");
} else {
//生成token,并保存到数据库
result = shiroService.createToken(user.getUserId());
result.put("status", 200);
result.put("msg", "登陆成功");
}
return result;
}
/**
* 退出
*/
@ApiOperation(value = "登出", notes = "参数:token")
@PostMapping("/sys/logout")
public Map<String, Object> logout(@RequestHeader("token")String token) {
Map<String, Object> result = new HashMap<>();
shiroService.logout(token);
result.put("status", 200);
result.put("msg", "您已安全退出系统");
return result;
}
}
通过自定义过滤器拦截,拒绝访问的请求通过executeLogin方法,校验是否能通过
**
* Shiro自定义auth过滤器
*
*/
@Component
public class AuthFilter extends AuthenticatingFilter {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 生成自定义token
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token
String token = TokenUtil.getRequestToken((HttpServletRequest) request);
return new AuthToken(token);
}
/**
* 步骤1.所有请求全部拒绝访问
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
return false;
}
/**
* 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//获取请求token,如果token不存在,直接返回
String token = TokenUtil.getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
httpResponse.setCharacterEncoding("UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("status", 403);
result.put("msg", "请先登录");
String json = MAPPER.writeValueAsString(result);
httpResponse.getWriter().print(json);
return false;
}
return executeLogin(request, response);
}
/**
* token失效时候调用
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
httpResponse.setCharacterEncoding("UTF-8");
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
Map<String, Object> result = new HashMap<>();
result.put("status", 403);
result.put("msg", "登录凭证已失效,请重新登录");
String json = MAPPER.writeValueAsString(result);
httpResponse.getWriter().print(json);
} catch (IOException e1) {
}
return false;
}
}
查看源码,exeuteLogin内部使用的就是shiro的登录方法
自定义token类要继承UsernamePasswordToken
/**
* Shiro自定义token类
*
*/
public class AuthToken extends UsernamePasswordToken {
private String token;
public AuthToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
这里不继承 AuthenticationToken,因为getAuthenticationTokenClass()实际上获取到的是UsernamePasswordToken.class
用户 | 角色 | 权限 |
---|---|---|
admin | admin | [查看文件,拷贝文件、移动文件、重命名文件、下载文件、删除文件、上传文件、目录管理、创建目录、文件打分、评论文件、查看得分] |
lyp | user | [查看文件,拷贝文件、下载文件、上传文件、目录管理、评论文件、查看得分] |
/**
* shiro配置类
*
*/
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 登录配置
shiroFilter.setLoginUrl("/login");
shiroFilter.setSuccessUrl("/");
shiroFilter.setUnauthorizedUrl("/error/403");
// 自定义过滤器
Map<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("mlfc", new MyLoginFilter());
shiroFilter.setFilters(filtersMap);
// 拦截配置
Map<String, String> filterChainDefinitions = new LinkedHashMap<>();
filterChainDefinitions.put("/assets/**", "anon");
filterChainDefinitions.put("/login", "anon");
filterChainDefinitions.put("/reg", "anon");
filterChainDefinitions.put("/logout", "logout");
filterChainDefinitions.put("/**", "mlfc,user");
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitions);
return shiroFilter;
}
@Bean(name = "userRealm")
@DependsOn("lifecycleBeanPostProcessor")//依赖于另一个组件,也就是说被依赖的组件会比该组件先注册到IOC容器中。
public MyRealm userRealm() {
MyRealm myRealm = new MyRealm();//自定义的Realm
myRealm.setCredentialsMatcher(credentialsMatcher()); // 将加密规则注入Realm
return myRealm;
}
//SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());//将Realm注入到SecurityManager中。
securityManager.setCacheManager(cacheManager());//注入缓存对象。
securityManager.setRememberMeManager(rememberMeManager());//注入rememberMeManager()
return securityManager;
}
@Bean(name = "cacheManager")
public EhCacheManager cacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:shiro/ehcache-shiro.xml");
return cacheManager;
}
// 自定义加密规则
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(3);
return credentialsMatcher;
}
//负责管理shiro的生命周期,项目中不加lifecyclebeanpostprocessor也没问题
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor();
return lifecycleBeanPostProcessor;
}
/**
* shiro里实现的Advisor类,用来拦截注解的方法 .
*
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
/**
* 实现spring的自动代理
* 这个类将扫描上下文,寻找所有的Advistor(一个Advisor是一个切入点和一个通知的组成),将这些Advisor应用到所有符合切入点的Bean中,
* 即为匹配的目标Bean自动创建代理
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 配置ShiroDialect:用于thymeleaf和shiro标签配合使用
*/
@Bean("shiroDialect")
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
//cookie对象
@Bean
public SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
//cookie管理对象 处理记住我功能
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;
}
}
/**
* 自定义认证授权处理逻辑
*
*/
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RoleAuthMapper roleAuthMapper;
/**
* 授权 获取用户的角色和权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
List<String> roleList = userRoleMapper.selectRoleCodesById(user.getId());
List<String> authList = roleAuthMapper.selectAuthCodesByRoleCodes(roleList);
Set<String> roleSet = new HashSet<>(roleList);
Set<String> authSet = new HashSet<>(authList);
authorizationInfo.setRoles(roleSet);
authorizationInfo.setStringPermissions(authSet);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
// 往数据库中查询用户
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getUsername, username));
if (user == null) {
// 账号不存在
throw new UnknownAccountException();
}
// 密码验证shiro处理
return new SimpleAuthenticationInfo(
user, //用户
user.getPassword(), //密码
ByteSource.Util.bytes("lyp"), // 加密传入盐值
getName());//Realm name
);
}
}
@RestController
public class ScoreController extends BaseController{
@Autowired
ScoreServiceImpl scoreService;
@Autowired
UserService userService;
/**
* 文件打分
* @param score
* @return
*/
@RequiresPermissions("file:score")
@PostMapping("/addScore")
public R addScore(Score score){
System.out.println("获取结果:"+score);
score.setUserId(getLoginUserId());
if(scoreService.addScore(score)){
return R.succeed("打分成功");
}
return R.failed("打分失败");
}
/**
* 获取文件得分
*/
@RequiresPermissions("file:check")
@GetMapping("/getScore/{fileId}")
public R getScore(@PathVariable Long fileId){
Score score = scoreService.queryScoreByFId(fileId);
return R.succeed(score,"查询成功");
}
}
/**
* 登录
*/
@PostMapping("/login")
@ResponseBody
public R login(HttpServletRequest request, String username, String password, String code, boolean rememberMe) {
if (StringUtil.isBlank(username, password)) {
return R.failed("账号或密码不能为空");
}
String sessionCode = (String) request.getSession().getAttribute("captcha");
if (code == null || !sessionCode.equals(code.trim().toLowerCase())) {
return R.failed("验证码不正确");
}
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
try {
//执行登录方法,如果没有异常就说明OK了
subject.login(token);
return R.succeed("登录成功");
} catch (UnknownAccountException e) {
return R.failed("用户不存在");
} catch (IncorrectCredentialsException e) {
return R.failed("密码错误");
} catch (ExcessiveAttemptsException eae) {
return R.failed("操作频繁,请稍后再试");
}
}
引入标签
<%@ taglib uri=“http://shiro.apache.org/tags” prefix=“shiro”>
查看a>