你们要的Shiro权限分离入门就在这里!

阅读本文约“12分钟”

适读人群:Java中级

猫叔:哟,最近断更了一段时间,后续会继续和大家调侃,今天给大家推荐一篇基于Shiro的入门项目,小纯洁是我老友,编码4年有余,本篇知识点集中、且案例基础完善,支持一波,最后如果有读者可以来波总结留言就更好了!

原标题:《基于SpringBoot+Shiro搭建的前后端分离鉴权架构》

攝影師:Maksim Goncharenok,連結:Pexels

一、Shiro简介

为何选择Shiro?Apache Shiro是一个强大且易用的Java安全框架。开发者使用shiro可以轻松完成身份验证、授权、密码和会话管理。

Shiro的主要API

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

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

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

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

Web Support:Web支持,可以非常容易的集成到Web环境;

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

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

Testing:提供测试支持;

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

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

Shiro如何工作?

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

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

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

二、核心代码

1.登录/登出功能

假设我们有两个用户:


admin1: 用户名/密码为admin1/admin时成功,返回身份信息以及sessionId;
admin2: 用户名/密码为admin2/admin时成功,返回身份信息以及sessionId;
之后需要权限控制的接口必须在http请求头header带上sessionId(Authorization:sessionId的格式)。

登录/登出代码:

 1@PostMapping("/login")
 2public Object login(@RequestBody LoginVo loginVo) {
 3    //得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
 4    Subject subject = SecurityUtils.getSubject();
 5    UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getAccount(), loginVo.getPassword());
 6    token.setRememberMe(false);
 7    if (subject.isAuthenticated()) {
 8        subject.logout();
 9    }
10    try {
11        //登录,即身份验证
12        subject.login(token);
13        Session session = subject.getSession();
14        User user = User.loginUser();
15        user.setFlag(loginVo.getFlag());
16        user.setSessionId(session.getId());
17        //返回一个sessionId
18        return user;
19    } catch (UnknownAccountException e){
20        return "账号/密码错误";
21    }
22    catch (AuthenticationException e) {
23        //身份验证失败
24        return "程序错误";
25    }
26}
27
28@PostMapping("/logout")
29public Object logout() {
30    try {
31        Subject subject = SecurityUtils.getSubject();
32        subject.logout();
33        return "成功退出登录!";
34    } catch (Exception e) {
35        return "退出登录失败!";
36    }
37}

2.Shiro配置

配置shiro的拦截器,以及开启@RequiresPermissions 注解支持

 1@Configuration
 2public class ShiroConfig {
 3
 4@Bean
 5public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
 6    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
 7
 8    factoryBean.setSecurityManager(securityManager);
 9
10    // 自定义拦截器
11    Map<String, Filter> filterMap = new LinkedHashMap<>();
12    filterMap.put("apiPathPermissionFilter", new ApiPathPermissionFilter());
13    factoryBean.setFilters(filterMap);
14
15    Map<String, String> filterRuleMap = new LinkedHashMap<>();
16    //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
17    filterRuleMap.put("/logout", "logout");
18    // 配置不会被拦截的链接 顺序判断
19    filterRuleMap.put("/login", "anon");
20    // 其他请求通过我们自己的apiPathPermissionFilter
21    filterRuleMap.put("/*", "apiPathPermissionFilter");
22    filterRuleMap.put("/**", "apiPathPermissionFilter");
23    filterRuleMap.put("/*.*", "apiPathPermissionFilter");
24    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
25
26    return factoryBean;
27}
28
29/**
30 * SecurityManager安全管理器,是Shiro的核心
31 */
32@Bean
33public SecurityManager securityManager(MyRealm myRealm) {
34    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
35    //设置session生命周期
36    securityManager.setSessionManager(sessionManager());
37    // 设置自定义 realm
38    securityManager.setRealm(myRealm);
39    return securityManager;
40}
41
42/**
43 * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
44 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
45 */
46@Bean
47public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
48    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
49    // 强制使用cglib,防止重复代理和可能引起代理出错的问题
50    // https://zhuanlan.zhihu.com/p/29161098
51    defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
52    return defaultAdvisorAutoProxyCreator;
53}
54
55/**
56 * 开启aop注解支持
57 */
58@Bean
59public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
60    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
61    advisor.setSecurityManager(securityManager);
62    return advisor;
63}
64
65/**
66 * 自定义sessionManager
67 */
68@Bean
69public SessionManager sessionManager() {
70    //由shiro管理session,每次访问后会重置过期时间
71    MySessionManager mySessionManager = new MySessionManager();
72    //设置过期时间,单位:毫秒
73    mySessionManager.setGlobalSessionTimeout(MySessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT);
74    return mySessionManager;
75}
76
77
78/**
79 * LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部,
80 * 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的
81 */
82@Bean
83public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
84    return new LifecycleBeanPostProcessor();
85}
86}

3.sessionId获取

实现自己的一套session管理器,需重写DefaultWebSessionManager

 1/**
 2 *
 3 * 功能描述: 自定义sessionId获取,用于请求头中传递sessionId,并让shiro获取判断权限
 4 * 想实现自己的一套session管理器,需继承DefaultWebSessionManager来重写
 5 *
 6 * @param:
 7 * @return:
 8 * @auther: liyiyu
 9 * @date: 2020/3/17 17:05
10 */
11 public class MySessionManager extends DefaultWebSessionManager {
12
13     private static final String AUTHORIZATION = "Authorization";
14
15     public MySessionManager() {
16         super();
17     }
18
19     @Override
20     protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
21        //修改shiro管理sessionId的方式,改为获取请求头,前端时header必须带上 Authorization:sessionId
22        return WebUtils.toHttp(request).getHeader(AUTHORIZATION);
23      }
24  }

4.登录实现以及权限控制

doGetAuthenticationInfo是登录的具体实现,通过查询数据库匹配账号和加密后的密码来判断是否正确

doGetAuthorizationInfo是权限判断的具体实现,通过获取当前请求的用户来判断用户的权限
我们设置admin1 拥有poetry1 poetry2的权限,admin2拥有poetry3 poetry4的权限
 1@Component
 2public class MyRealm extends AuthorizingRealm {
 3
 4@Value("${password_salt}")
 5private String salt;
 6
 7/**
 8 * AuthorizationInfo 用于聚合授权信息
 9 * 会判断@RequiresPermissions 里的值是否
10 *
11 */
12@Override
13protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
14    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
15    //进入数据库查询拥有的权限查询
16    List<String> list = new ArrayList<>();
17    User user = (User)SecurityUtils.getSubject().getPrincipal();
18    if ("1".equals(user.getRoleId())){
19        String[] array = {"poetry1","poetry2"};
20        list  = Arrays.asList(array);
21    }else if ("2".equals(user.getRoleId())){
22        String[] array = {"poetry3","poetry4"};
23        list  = Arrays.asList(array);
24    }
25
26    Set<String> set = new HashSet(list);
27    info.addStringPermissions(set);
28    return info;
29}
30
31@Override
32protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
33    String account = (String) authenticationToken.getPrincipal();  //得到用户名
34    String pwd = new String((char[]) authenticationToken.getCredentials()); //得到密码
35    //将密码进行加密处理,与数据库加密后的密码进行比较
36    String inPasswd = PasswordUtils.entryptPasswordWithSalt(pwd, salt);
37    //通过数据库验证账号密码,成功的话返回一个封装的ShiroUser实例
38    String saltPasswd = PasswordUtils.entryptPasswordWithSalt("admin", salt);
39    User user = null;
40    //这里要注意返回用户信息尽可能少,返回前端所需要的用户信息就可以了
41    if ("admin1".equals(account) && saltPasswd.equals(inPasswd)) {
42        user = new User();
43        user.setUid("1");
44        user.setUname("用户一");
45        user.setEid(1);
46        user.setDeptName("祖安大区");
47        user.setDeptId("1");
48        user.setRoleId("1");
49        user.setRoleName("祖安文科状元");
50    }else if ("admin2".equals(account) && saltPasswd.equals(inPasswd)){
51        user = new User();
52        user.setUid("1");
53        user.setUname("用户二");
54        user.setEid(1);
55        user.setDeptName("祖安大区");
56        user.setDeptId("1");
57        user.setRoleId("2");
58        user.setRoleName("祖安理科状元");
59    }
60    if (user != null) {
61        //如果身份认证验证成功,返回一个AuthenticationInfo实现;
62        return new SimpleAuthenticationInfo(user, pwd, getName());
63    } else {
64        //错误的帐号
65        throw new UnknownAccountException();
66    }
67}
68}

5.跨域问题

在开发过程中出现了跨域的问题

 1@Configuration
 2public class MvcConfiguration extends WebMvcConfigurerAdapter {
 3
 4@Override
 5public void addInterceptors(InterceptorRegistry registry) {
 6    //为所有请求处理跨域问题
 7    registry.addInterceptor(new UrlInterceptor()).addPathPatterns("/**");
 8    super.addInterceptors(registry);
 9}
10}

 1public class UrlInterceptor extends HandlerInterceptorAdapter {
 2
 3@Override
 4public boolean preHandle(HttpServletRequest request,
 5                         HttpServletResponse response, Object handler) throws Exception {
 6    //允许跨域,不能放在postHandle内
 7    response.setHeader("Access-Control-Allow-Credentials", "true");
 8    String str = request.getHeader("origin");
 9    response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
10    response.setHeader("Cache-Control", "no-cache");
11    response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT");
12    response.setHeader("Access-Control-Max-Age", "0");
13    response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,Authorization,WG-App-Version, WG-Device-Id, WG-Network-Type, WG-Vendor, WG-OS-Type, WG-OS-Version, WG-Device-Model, WG-CPU, WG-Sid, WG-App-Id, WG-Token");
14    response.setHeader("XDomainRequestAllowed", "1");
15    return true;
16}
17}

5.验证权限

通过@RequiresPermissions 来验证权限(ShiroConfig类配置)

 1@GetMapping("/poetry1")
 2@RequiresPermissions("poetry1")
 3public Object poetry1(){
 4    return "床前明月光";
 5}
 6
 7@GetMapping("/poetry2")
 8@RequiresPermissions("poetry2")
 9public Object poetry2(){
10    return "疑是地上霜";
11}
12
13@GetMapping("/poetry3")
14@RequiresPermissions("poetry3")
15public Object poetry3(){
16    return "举头望明月";
17}
18
19@GetMapping("/poetry4")
20@RequiresPermissions("poetry4")
21public Object poetry4(){
22    return "低头思故乡";
23}

具体代码:https://github.com/leeyiyu/shiro_token
参考文档:https://www.iteye.com/blog/jinnianshilongnian-2018398

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值