1.Shiro架构图


Shiro[笔记] - 图1

Authentication
身份认证/登录,验证用户是不是拥有相应的身份.

Authorization
授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager
会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;

Cryptography
加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support
Shiro的web支持的API能够轻松地帮助保护 Web 应用程序。主要就是用来对Web程序进行一个好的支持的。

Caching

缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率

Concurrency
shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去.

Testing
提供测试支持;

Run As
允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me
记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

2.核心API


Subject



Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;


SecurityManager

安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。

SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager


Realm
Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
Shiro[笔记] - 图2

3.工作流程图

Shiro[笔记] - 图3

1、登陆操作 携带用户名密码给subject,subject调用自己的登陆方法传递用户名和密码给权限管理器,权限管理器将用户名密码传递给开发人员编写的realm的认证方法,realm根据用户名到数据库中查找是否存在该用户,若存在将认证信息存入到session中.

2、权限管理器会自动判断传递的密码与正确密码是否一致

3、访问3类资源(页面) 过滤器寻找权限管理器判断该用户是否拥有xxx权限,权限管理器从session中取出认证信息对象,返回给realm,realm判断该用户拥有什么权限,封装到授权信息中返回给权限管理器,权限管理器将判断的结果返回给过滤器

4、访问3类资源(xxx添加需要访问service)(对于过滤器来说属于2类资源),在执行方法时,会到达前置通知(esrvice方法上添加注解@RequiresPermissions(“courier:list”)),权限通知寻找权限管理器判断该用户是否拥有xxx权限,权限管理器从session中取出认证信息对象,返回给realm,realm判断该用户拥有什么权限,封装到授权信息中返回给权限管理器,权限管理器将判断的结果返回给权限通知

其实简单来说 /userAction_login —————>请求先到达权限过滤器shiroFilter,先判断是几类资源
登录属于一类资源直接放行到—————>userActon中(userAction中调用执行subject对象(使用入口是一个操作入口对象,里面有登陆方法,登出方法,获取当前对象方法)的登陆方法subject.login方法(携带着用户名,密码)
————>subject对象调用 securityManager的login方法 权限管理器不能判断用户和密码是对的需要
————>ream认证|授权器(开发人员编写,判断用户名是否存在,拥有什么权限)————>处理完后把认证信息对象返回给securityManager()如果认证信息没有问题,权限管理器会把认证信息存入session(证明认证登陆过了)

4.SpringBoot整合Shiro


1. 添加坐标依赖
2. 编写Shiro配置文件
需要ShiroConfig
需要创建三个Bean
ShiroFilterFactoryBean
需要设置安全管理器(setSecurityManager())

Shiro内置过滤器,可以实现权限相关的拦截器
常用的过滤器:
anon: 无需认证(登录)可以访问
authc: 必须认证才能访问(也可以使用通配符的方式,比如 /user/*)
user: 如果使用了rememberMe功能可以直接访问
perms: 该资源必须得到资源权限才可以访问
role: 该资源必须得到角色权限才可以访问


DefaultWebSecurityManager
需要关联Realm类


Realm类(授权认证功能)


5.Realm类的编写


需要继承AuthorizingRealm 重写两个方法

doGetAuthenticationInfo 执行认证逻辑

doGetAuthorizationInfo 执行授权逻辑




6.如果自定义异常抛不出去(被源码里面的上层捕获了)


Shiro[笔记] - 图4

7.登录方法

Shiro[笔记] - 图5
UnknownAccountException 是用户名不存在

IncorrectCredentialsException 密码错误异常

8.设置未授权提示页面

setUnauthorizedUrl()


9.用户访问无权限页面给与提示


/**

*/
@ControllerAdvice

public class MyExceptionHandler {

/**

  1. * 拦截无权限异常
  2. */

@ExceptionHandler(AuthorizationException.class)

@ResponseBody

public ExceptionResponseEntity AuthorizationException(Exception e) {

  1. **return new **ExceptionResponseEntity(ShiroExceptionEnum.**_NO_PERMISSION_**.getCode(), ShiroExceptionEnum.**_NO_PERMISSION_**.getMessage());

}

}


访问无权限的效果:
{
“code”: 701,
“message”: “您无权限访问”
}



10.Shiro的异常


DisabledAccountException(禁用的帐号)、
LockedAccountException(锁定的帐号)、
UnknownAccountException(错误的帐号)、
ExcessiveAttemptsException(登录失败次数过多)、
IncorrectCredentialsException (错误的凭证)、
ExpiredCredentialsException(过期的凭证)等,
AuthorizationException 访问没权限的接口会抛这个异常, 比如说,user权限去访问admin

11.未登录状态访问无权限接口给与登录提示[前后端分离项目无效]

需要在ShiroConfig配置类的 ShiroFilterFactoryBean 的Bean内部配置shiroFilterFactoryBean.setLoginUrl(“/toLogin”);
然后在指定这个

注意得是Get请求才行, Post请求可能不行

| @GetMapping(“/toLogin”)
public String toLogin() {
System.out.println(“没有登录”);
return “没有登录,请登录”;
} | | —- |


12.注解方式拦截url请求

  1. 在ShiroConfig配置类里面添加两个Bean

| /**

  • 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
  • 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
    *
  • @return
    *
    /
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    advisorAutoProxyCreator.setProxyTargetClass(true);
    return advisorAutoProxyCreator;
    }

    /**
  • 开启aop注解支持
    *
  • @return
    *
    /
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    authorizationAttributeSourceAdvisor.setSecurityManager(this.securityManager());
    return authorizationAttributeSourceAdvisor;
    } | | —- |



    然后在类上面添加@RequiresRoles(“admin”)

    整个类就需要有admin才能访问,如果没有admin权限访问就会报错信息.


    13.Rest无状态关闭session创建




    第五需要StatelessDefaultSubjectFactory:由于我们编写的是无状态的,每人情况是会创建session对象的,那么我们需要修改createSubject关闭session的创建。

    14.自定义密码比较器

    那个有点麻烦, 只需要在login 方法把接收的密码提前进行加密传给Shiro即可

    @GetMapping(“/login”)

public String login(String userName, String passWord) {

Subject subject = SecurityUtils.getSubject();
//2.封装用户数据(传入的密码是进行加密处理的)

UsernamePasswordToken token = new UsernamePasswordToken(userName, AESUtils.encryption(passWord));

try {

  1. subject.login(token);<br /> } **catch **(IncorrectCredentialsException e) {
  2. **throw new **ExceptionUtil(**"密码输入错误"**);

}

return “登录成功”;

}






15.shiroConfig配置文件无法获得yml里面的值的问题

解决方法:将LifecycleBeanPostProcessor的配置方法改成静态的就可以了

@Bean

public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {

return new LifecycleBeanPostProcessor();

}

16.未登录状态返回json而不是login.jsp


在非token应用中,自定义过滤器是未登录状态下会访问login.jsp,而不是返回json格式字符串的,这样的话在前后端分离是不行的,因为前端需要根据返回code来判断是跳着到哪个页面的

https://www.yuque.com/docs/share/a920ba02-0cca-4a9e-8e08-5b863e55b902?#


(二)授权

1.注解授权

注解名 作用
@RequiresAuthentication 作用于类上,方法,实例上,调用时,当前的subject是必须经过了认证的
@RequiresGuest 代表无需认证即可访问.
@RequiresPermissions 作用于类,方法,实例上,调用时需要判断subject中是否包含当前接口中的Permission(权限信息).
@RequiresRoles 作用于的类,方法,实例上,调用时,需要判断subject中是否包含当前接口中的Role(角色信息).
@RequiresUser 作用于的类,方法,实例上,调用时,需要判断subject中是否当前应用中的用户.



@RequiresRoles({“user”, “admin”}) //同时具有admin和user权限才能查看,少一个都不能查看


//只有user 权限可以访问,或者只有admin权限也可以访问 ,注意logical = Logical.OR 一定不能忘记

@RequiresRoles(value = {“user”, “admin”}, logical = Logical.OR)





(三)其它

1.shiro练习项目

Shiro[笔记] - 图6 有sql脚本 好像是整合了jwt


(四)放行Swagger


在 Shiro 的配置文件中找到拦截器,将Swagger接口的路径放行即可

| @Bean
public ShiroFilterFactoryBean webFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager());
//配置拦截链 使用LinkedHashMap,因为LinkedHashMap是有序的,shiro会根据添加的顺序进行拦截
// Map K指的是拦截的url V值的是该url是否拦截
Map filterChainMap = new LinkedHashMap(16);
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问,先配置anon再配置authc。
filterChainMap.put(“/login”, “anon”);
//放行Swagger2页面,需要放行这些
filterChainMap.put(“/swagger-ui.html”, “anon”);
filterChainMap.put(“/swagger/“, “anon”);
filterChainMap.put(“/webjars/
“, “anon”);
filterChainMap.put(“/swagger-resources/“, “anon”);
filterChainMap.put(“/v2/
“, “anon”);
filterChainMap.put(“/static/“, “anon”);

//
2019/12/16 暂时 放行所有的请求
// filterChainMap.put(“/“, “authc”);
//设置拦截请求后跳转的URL.
shiroFilterFactoryBean.setLoginUrl(“/login”);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
} | | —- |

注意: 该配置必须放置在以下代码之前,不然不会生效:
filterChainMap.put(“/**”, “authc”);



authc :所有url都必须认证通过才可以访问;
anon :所有url都都可以匿名访问

 内置过滤器


内置过滤器分为两组:
认证过滤器:
anon authcBaisc ,auchc , user

授权过滤器:
perms , roles ,ssl ,rest ,port

Shiro[笔记] - 图7

文字版简介:

https://www.cnblogs.com/koal/p/5152671.html



 注解

参考:
https://www.cnblogs.com/hunmeng/p/11032014.html

(一)使用注解前需要开启注解支持配置

使用注解前需要开启注解支持

在ShiroConfig 的Configuration 类里面配置

| /**

  • 下面的代码是添加注解支持 /
    @Bean
    @DependsOn(“lifecycleBeanPostProcessor”)
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    // 强制使用cglib,防止重复代理和可能引起代理出错的问题 // https://zhuanlan.zhihu.com/p/29161098
    defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
    return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    *return
    advisor;
    } | | —- |


    (二)注解有处理顺序的

    Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):
    RequiresRoles
    RequiresPermissions
    RequiresAuthentication
    RequiresUser
    RequiresGuest


    例如:你同时声明了RequiresRoles和RequiresPermissions,那就要求拥有此角色的同时还得拥有相应的权限。

    (三)@RequiresAuthentication


    已经登录才能访问的接口

    使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证(也就是已经登录过了)

    等同于方法subject.isAuthenticated() 结果为true时。



    (四)@RequiresPermissions



    当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行,会抛出异常出来


    与 RequiresRoles类似

    作用:
    当前的用户的Subject必须有指定的权限才能执行被这个注解标注的Controller方法,如果这个当前用户没有相关权限,那么这个方法不会去执行


    @RequiresPermissions(“index:hello”)
    @RequiresPermissions({“index:hello”,”index:world”})

    //两者有一个权限就行

    @RequiresPermissions(value={“index:hello”,”index:world”},logical=Logical.OR)


    使用方式:
    1. 需要在继承AuthorizingRealm 的类里面的doGetAuthorizationInfo 方法返回SimpleAuthorizationInfo的里面setStringPermissions()方法里面添加权限
    2. 然后在指定的方法上添加注解来限制这个接口. 只有有相关的Permissions权限的用户才能访问这个接口.



    (五)@RequiresRoles


    当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。

可以用在Controller或者方法上。可以多个roles,多个roles时默认逻辑为 AND也就是所有具备所有role才能访问。@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface RequiresRoles { String[] value();Logical logical() default Logical.AND;}• 示例//属于user角色@RequiresRoles(“user”)//必须同时属于user和admin角色@RequiresRoles({“user”,”admin”})//属于user或者admin之一;修改logical为OR 即可@RequiresRoles(value={“user”,”admin”},logical=Logical.OR)


(六)@RequiresUser


当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

@RequiresUser
验证用户是否被记忆,user有两种含义:
一种是成功登录的(subject.isAuthenticated() 结果为true);
另外一种是被记忆的(subject.isRemembered()结果为true)。


(七)@RequiresGuest


使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。


@RequiresGuest

验证是否是一个guest的请求,与@RequiresUser完全相反。
换言之,RequiresUser == !RequiresGuest。
此时subject.getPrincipal() 结果为null.




 缓存机制


SpringBoot 加token环境下整合Ehcache,实现用户授权信息缓存到数据库里面.

案例:ZJJ_Shiro_2020/02/20_15:18:01_d275i



(一)根据token清理用户的缓存


适合token整合的环境下来使用.

下面方法需要在 AuthorizingRealm 的子类去编写这个方法,

/
清理shiro的缓存信息
重新赋值权限(在比如:给一个角色临时添加一个权限,需要调用此方法刷新权限,否则还是没有刚赋值的权限)

@param token token
*/
public void reloadAuthorizingCacheByToken(String token) {
Subject subject = SecurityUtils.getSubject();
String realmName = subject.getPrincipals().getRealmNames().iterator().next();
//第一个参数为用户名,第二个参数为realmName,test想要操作权限的用户
SimplePrincipalCollection principals =
new SimplePrincipalCollection(token, realmName);
subject.runAs(principals);
super**.getAuthorizationCache().remove(subject.getPrincipals());
subject.releaseRunAs();
}


(二)退出登录同时清理缓存

customRealm.reloadAuthorizingCacheByToken(token);调用的是上面标题的方法

/
退出登录会清理ehcache 缓存里面的东西.

*
@return
*/
@ApiOperation(value = “退出登录功能”)
@GetMapping(“/logout”)
public SuccessVO logout(@Autowired CustomRealm customRealm, @RequestHeader String token) {
try {
Subject currentUser = SecurityUtils.getSubject();
currentUser.logout();
customRealm.reloadAuthorizingCacheByToken(token); //清理缓存
}
catch (Exception e) {
}
return new** SuccessVO();
}





 RememberMe

如果不使用RememberMe功能的话,当前浏览器登录完,进行操作之后,关闭浏览器再打开之后还需要重新登录才行.



 解决Vue跨域问题

  1. 安装插件
    在WebStorm 的Terminal面板里面输入命令

    npm install —save axios vue-axios

    开始安装插件,安装完成之后,重启项目.

    2. 修改vue项目的vue.config.js
module.exports = { baseUrl: ‘./‘, assetsDir: ‘static’, productionSourceMap: false, devServer: { open: true, //浏览器自动打开页面 host: “0.0.0.0”, //如果是真机测试,就使用这个IP port: 8080, https: false, hotOnly: false, //热更新(webpack已实现了,这里false即可) proxy: { //配置跨域 ‘/api’: { target: “http://192.168.3.171:8080/“, ws:true, changOrigin:true, pathRewrite:{ ‘^/api’:’/‘ } } } } }



然后你vue项目的需要调用接口的地方

| userMessage(formName) {
var res = this;
this.$axios({
// 这里对应着是vue.config.js配置的proxy配置的路径
url: “/api/login”,
method: “post”,

  1. **data**: {<br />
  2. **username**: res.**ruleForm**.**username**,<br />
  3. **password**: res.**ruleForm**.**password<br />
  4. **}<br />

})
.then(function(response) {
console.log(response);
if (response.data.code <= 200) {

  1. res.**$router**.push(**'/userManage'**);<br />
  2. } **else **{<br />
  3. _alert_(response.**data**.**message**);<br />
  4. }<br />
  5. })<br />
  6. .catch(**function**(error) {<br />
  7. res.**$message**.error(**"系统错误: " **+ error);<br />
  8. **_console_**.log(error);<br />
  9. });<br />

} | | —- |


·