一步一步地教你使用SpringBoot集成Shiro

章节目录

  • 1. Shiro
    • 1.1 Shiro简介
    • 1.2 Shiro三大核心组件
      • 1.2.1 Subject
      • 1.2.2 SecurityManager
      • 1.2.3 Realm
    • 1.3 Shiro的认证和授权流程
      • 1.3.1 认证流程
      • 1.3.2 授权流程
  • 2. SpringBoot集成Shiro并完成登录操作
    • 2.1 项目信息
      • 2.1.1 开发环境
      • 2.1.2 项目结构图
      • 2.1.3 pom依赖
    • 2.2 集成Shiro

1. Shiro

Shiro官网

1.1 Shiro简介

以下为Shiro的简单介绍:

  1. Shiro 是一个强大、简单易用的 Java 安全权限框架
  2. Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE 环境,也可以用在 JavaEE 环境
  3. Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存等。

具体的信息可进一步查看Shiro官网

1.2 Shiro三大核心组件

Shiro 有三大核心组件,即 Subject、SecurityManager 和 Realm。它们的关系如下图:
一步一步地教你使用SpringBoot集成Shiro_第1张图片

1.2.1 Subject

Subject 为认证主体,它包含 Principals 和 Credentials 两个信息。

  • Principals:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登录主体的身份
  • Credentials:代表凭证。常见的有密码,数字证书等等。

简单来说:这两者代表了需要认证的内容,最常见的便是用户名、密码了

1.2.2 SecurityManager

SecurityManager 为安全管理员,它是Shiro 架构的核心。所有具体的交互都通过SecurityManager进行控制;负责所有Subject、且负责进行认证和授权、及会话、缓存的管理

1.2.3 Realm

Realm 是一个域,它是连接 Shiro 和具体应用的桥梁。当需要与安全数据交互时,比如用户账户、访问控制等,Shiro 将会在一个或多个 Realm 中查找。

1.3 Shiro的认证和授权流程

官方的入门实例:http://shiro.apache.org/tutorial.html
大致代码如下:
shiro.ini

[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

ini文件大致意思是:

  1. root=secret,admin:用户名root,密码secret,角色是admin
  2. schwartz=lightsaber:* :角色schwartz拥有权限 lightsaber:*

Java代码

public static void main(String[] args) {
	Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
	SecurityManager securityManager = factory.getInstance();
	SecurityUtils.setSecurityManager(securityManager);
	
	Subject currentUser = SecurityUtils.getSubject();
	
	if ( !currentUser.isAuthenticated() ) {
		UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
	    token.setRememberMe(true);
	    currentUser.login(token);
	}
	
	if ( currentUser.hasRole( "schwartz" ) ) {
	    log.info("May the Schwartz be with you!" );
	} else {
	    log.info( "Hello, mere mortal." );
	}
	
	if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
	    log.info("You may use a lightsaber ring.  Use it wisely.");
	} else {
	    log.info("Sorry, lightsaber rings are for schwartz masters only.");
	}
	...
}

上面代码主要有两个步骤:
认证:

UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
currentUser.login(token);

判断权限:

currentUser.hasRole( "schwartz" )

1.3.1 认证流程

一步一步地教你使用SpringBoot集成Shiro_第2张图片
shiro的认证流程如下:

  1. Subject进行login操作,参数是封装了用户信息的token
  2. Security Manager进行登录操作
  3. Security Manager委托给Authenticator进行认证逻辑处理
  4. 调用AuthenticationStrategy进行多Realm身份验证
  5. 调用对应Realm进行登录校验,认证成功则返回用户属性,失败则抛出对应异常

1.3.2 授权流程

一步一步地教你使用SpringBoot集成Shiro_第3张图片
授权流程如下:

  1. 调用Subject.isPermitted()/hasRole()方法
  2. 委托给SecurityManager
  3. 而SecurityManager接着会委托给Authorizer
  4. Authorizer会判断Realm的角色/权限是否和传入的匹配
  5. 匹配如isPermitted/hasRole会返回true,否则返回false表示授权失败

2. SpringBoot集成Shiro并完成登录操作

2.1 项目信息

2.1.1 开发环境

IDEA:2018.2(lombok插件)
SpringBoot:2.3.1.RELEASE
Shiro:1.3.2
freemarker(前端页面)

2.1.2 项目结构图

一步一步地教你使用SpringBoot集成Shiro_第4张图片

2.1.3 pom依赖


    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.apache.shiro
        shiro-spring
        1.3.2
    
    
        org.springframework.boot
        spring-boot-starter-freemarker
    
    
    
        org.projectlombok
        lombok
        1.18.8
        provided
    

2.2 集成Shiro

根据官方文档 可以直接使用starter依赖包,但按照网上的搭建过程基本上是有坑的。所以,这里就使用了shiro-spring的依赖包咯(可查看这篇博客)。

步骤:
1)、导入依赖包
上述有。

2)、修改配置文件application.yml

spring:
  freemarker:
    suffix: .ftl
    template-loader-path: classpath:/templates/
    settings:
      #解决前台使用${}赋值值为空的情况
      classic_compatible: true

这里就是对前端页面freemarker的配置。至于对shiro的配置,就是通过SpringBoot中的配置类进行的。

3)、配置Shiro
配置shiro的securityManager和自定义realm。因为realm负责我们的认证与授权,所以是必须的,自定义的realm必须要交给securityManager管理,所以这两个类需要重写。然后还有一些资源的权限说明,所以一般需要定义ShiroFilterFactoryBean,所以有3个类我们常写的:

  • AuthorizingRealm:自定义realm
  • DefaultWebSecurityManager:shiro的核心管理器
  • ShiroFilterFactoryBean:过滤器链配置

4)、动手编码
ShiroConfig:

@Slf4j
@Configuration
public class ShiroConfig {
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm
        securityManager.setRealm(userRealm());
        return securityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 设置登录页面的url;如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 设置授权失败时应该跳转的页面(即:访问了权限不足的页面时,需要跳转到/role对应的url)
        shiroFilterFactoryBean.setUnauthorizedUrl("/role");
        shiroFilterFactoryBean.setSuccessUrl("/success");

        // 设置拦截器
        Map filterMap = new LinkedHashMap<>();
        // anon:表示可以匿名访问(不需要认证)
        filterMap.put("/guest/**", "anon");
        // 用户,需要角色权限 user
        filterMap.put("/user/**", "roles[user]");
        filterMap.put("/admin/**", "roles[admin]");
        // 开放登陆接口
        filterMap.put("/doLogin", "anon");
        filterMap.put("/login", "anon");
        // 其余接口一律拦截
        // 主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
        filterMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        log.info("Shiro拦截器工厂类注入成功");

        return shiroFilterFactoryBean;
    }
}

【注意】:SecurityManager 类导入的应该是 import org.apache.shiro.mgt.SecurityManager;

shirFilter()方法中主要设置了一些跳转url(如:没有登录时,会跳转到 /login 对应的页面)以及各类url的权限拦截(如:访问"/user"开头的url,需要"user"的角色;访问"/guest"开头的url,可以匿名访问)。

权限拦截 Filter
这里对Shiro中的Filter稍加说明一下:当运行一个Web应用程序时,Shiro将会创建一些有用的默认 Filter 实例,并自动地将它们置为可用,而这些默认的 Filter 实例是被 DefaultFilter 枚举类定义的。
一步一步地教你使用SpringBoot集成Shiro_第5张图片
对其中一些过滤器稍加解释一下:

Filter 解释
anon 无参,开放权限,可以理解为匿名用户或游客
authc 无参,需要认证
logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
user 无参,表示必须存在用户,当登入操作时不做检查
perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算通过
roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles[“admin,user”],当有多个参数时必须每个参数都通过才算通过

【注意】:anon, authc, authcBasic, user 是第一组认证过滤器;perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作。即:先要完成认证才能前去寻找授权,才能走第二组授权器。如:访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url

UserRealm:
要继承 AuthorizingRealm 类来自定义我们自己的 realm 以进行我们自定义的认证和授权操作

public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取授权用户信息(principalCollection存放了认证成功的用户信息)
        User user = (User) principalCollection.iterator().next();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // TODO 改为从数据库中获取该用户的角色
        authorizationInfo.addRole(user.getRole());
        return authorizationInfo;
    }
    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        String username = token.getUsername();
        String password = String.valueOf(token.getPassword());
        // TODO 改为从数据库获取对应用户名密码的用户
        User user = userService.getUserByName(username);
        if (null == user) {
            throw new UnknownAccountException("用户名不存在");
        } else if (!password.equals(user.getPassword())) {
            throw new IncorrectCredentialsException("密码不正确");
        }
        SecurityUtils.getSubject().getSession().setAttribute("user", user);
        return new SimpleAuthenticationInfo(user, password, getName());
    }
}

重写的两个方法分别是实现身份认证以及授权认证。

  1. doGetAuthenticationInfo():需要进行认证时,才会进入此方法。如调用:Subject.login(token)
  2. doGetAuthorizationInfo():需要授权认证时,才会进入此方法。如:配置类中配置了 ==filterMap.put("/admin/**", “roles[admin]”);==管理员的角色,这时,如果访问了以"/admin"开头的url,则会进入此方法。

UserServiceImpl:
UserServiceImpl是UserService接口的实现类,这里就贴出它的实现类的代码了。

@Service
public class UserServiceImpl implements UserService {
    private static Map userMap = new HashMap<>();
    static {
        userMap.put("zzc", new User("1", "zzc", "666", "user"));
        userMap.put("wzc", new User("2", "wzc", "888", "user"));
        userMap.put("yht", new User("3", "yht", "999", "admin!"));
    }

    @Override
    public User getUserByName(String username) {
        // Map通过key进行得到value
        return userMap.get(username);
    }
}

这里没使用数据库了,就直接使用了假数据,并存储在了Map中,通过静态代码块添加了部分数据。Map的key是id(这里假设id是与username 一样,方便调用getUserByName()),value是User。

User:

@Data
public class User {
    private String id;
    private String username;
    private String password;
    // 角色
    private String role;

    public User(String id, String username, String password, String role) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.role = role;
    }
}

好了,接下来就是controller层了。

GuestController:

@RestController
@RequestMapping("/guest")
public class GuestController {

    @GetMapping("/enter")
    public String enter() {
        return "欢迎进入,您的身份是游客";
    }
}

UserController:

@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/enter")
    public String enter() {
        return "欢迎进入,您的身份是用户";
    }
}

AdminController:

@RestController
@RequestMapping("/admin")
public class AdminController {
    @GetMapping("/enter")
    public String enter() {
        return "Admin enter!!";
    }
}

这上面Controller里面的内容基本一致,就是为了测试不同的url是由不同的角色才能访问的(接下去继续看)。

LoginController:

@Slf4j
@Controller
public class LoginController {
    @Autowired
    private HttpServletRequest request;
    
    // 进入登录页面
    @GetMapping("/login")
    public String login() {
        log.info("进入login()方法");
        return "login";
    }

    // 执行登录操作
    @PostMapping("/doLogin")
    public String doLogin(String username, String password) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        String errorMsg = null;
        try {
            SecurityUtils.getSubject().login(token);
        } catch (AuthenticationException e) {
            if (e instanceof UnknownAccountException) {
                errorMsg = "用户不存在";
            } else if (e instanceof LockedAccountException) {
                errorMsg = "用户被禁用";
            } else if (e instanceof IncorrectCredentialsException) {
                errorMsg = "密码错误";
            } else {
                errorMsg = "认证失败";
            }
            request.setAttribute("errorMsg", errorMsg);
            return "/login";
        }
        return "redirect:/success";
    }

    // 登录成功页面
    @GetMapping("/success")
    public String success() {
        return "success";
    }

    // 退出
    @GetMapping("/logout")
    public String logout() {
        log.info("进入logout()方法");
        SecurityUtils.getSubject().logout();
        return "redirect:/login";
    }
    
    // 授权失败时,跳转到role页面
    @GetMapping("/role")
    public String role() {
        return "role";
    }
}

login.ftl:
登录页面


用户登录

username:
password:

${errorMsg}

success.ftl:


登录成功:${user.username}

role.ftl:


权限不足

5)、测试
针对之前配置类的拦截器中的配置进行测试:

  1. 以"/guest"开头的url,匿名访问(不需要认证/登录)
filterMap.put("/guest/**", "anon");

一步一步地教你使用SpringBoot集成Shiro_第6张图片
访问:http://localhost:8080/guest/enter 的url,由于不需要认证,所以可以直接访问。

  1. 以"/user"开头的url,需要"user"的角色
filterMap.put("/user/**", "roles[user]");

一步一步地教你使用SpringBoot集成Shiro_第7张图片
上述的url还没有按回车键,按后:
一步一步地教你使用SpringBoot集成Shiro_第8张图片
跳转到了这个登录界面(按回车键,跳转到登录界面,这个过程,我无法证明,但读者可以去试试。)。话说回来,为什么会跳转到登录界面?因为访问以"/user"开头的url是需要"user"的角色的,都还没有登录,怎么可能会让你访问呢?所以,需要你去登录。

执行登录逻辑:
进行登录操作时,会调用LoginController.doLogin()方法,会执行

SecurityUtils.getSubject().login(token);

上面那条语句,所以,会调用UserRealm.doGetAuthenticationInfo()方法进行认证(如果忘记了,请往上看UserRealm类那块)。

我们以“zzc-666”的用户进行登录,它的角色是"user"。
先使用错的账号进行登录:
一步一步地教你使用SpringBoot集成Shiro_第9张图片
再来使用一个错的账号:
一步一步地教你使用SpringBoot集成Shiro_第10张图片
使用对的账号和密码:
一步一步地教你使用SpringBoot集成Shiro_第11张图片
在登录成功界面,我们来访问“进入管理员页面的”链接,它的html代码为:

进入管理员页面

根据配置类中的配置:

filterMap.put("/admin/**", "roles[admin]");

可知,访问此条链接是需要管理员角色的,那么我这个"user"角色能否访问?
一步一步地教你使用SpringBoot集成Shiro_第12张图片
由此可知,权限不足,无法访问,那么为何会跳转到这个页面呢?
根据此配置:

shiroFilterFactoryBean.setUnauthorizedUrl("/role");

好了,SpringBoot集成Shiro的简单介绍就到这儿了。

【参考资料】
教你 Shiro 整合 SpringBoot,避开各种坑

你可能感兴趣的:(Shiro)