写点什么

浅谈 Shiro 框架在 Spring Boot 中的认证应用

  • 2022 年 9 月 28 日
    北京
  • 本文字数:5233 字

    阅读完需:约 17 分钟

通常,公司的项目都会有严格的认证和授权操作,在 Java 开发领域常见的安全框架有 Shiro 和 Spring Security。Apache Shiro 是一个开源的轻量级 Java 安全管理框架,提供认证、授权、密码管理、缓存管理等功能,相对于 Spring Security 框架更加直观,易用,同时也能提供健壮的安全性。

对于 Spring Boot 项目,Shiro 官方提供了 shiro-spring-boot-web-starter 来简化 Shiro 在 Spring Boot 中的配置,不需要手动整合。

Shiro 核心组件

Shiro 有三大核心组件,即 Subject,SecurityManager 和 Realm,如图所示:



Spring Boot 整合 Shiro

1. 管理 shiro 版本号

<properties>    <shiro.version>1.6.0</shiro.version>    <java.version>1.8</java.version>    <jmeter.version>5.4.1</jmeter.version></properties><dependency>    <groupId>org.apache.shiro</groupId>    <artifactId>shiro-spring-boot-web-starter</artifactId>    <version>${shiro.version}</version></dependency>
复制代码

3. ShiroConfig 类

①. 创建 ShiroConfig 配置类,并添加注解 @Configuration

②. 在配置类中创建 3 个 Bean,ShiroFilterFactoryBean、DefaultWebSecurityManager 和 Realm


3.1 创建 Realm Bean

Realm Bean 是 ShiroConfig 配置类中的第 1 个 Bean,此处只展示一个 LdapReam Bean。注解 @DependsOn 表示组件依赖,下图中表示依赖 lifecycleBeanPostProcessor。LifecycleBeanPostProcessor 用来管理 shiro Bean 的生命周期,在 LdapReam 创建之前先创建 lifecycleBeanPostProcessor。


3.2 在 ShiroConfig 中添加 SecurityManager 配置

Shiro 通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。modularRealmAuthenticator 是 shiro 提供的 realm 管理器,用来设置 realm 生效, 通过 setAuthenticationStrategy 来设置多个 realm 存在时的生效规则。

@Bean(name = "securityManager")public DefaultWebSecurityManager securityManager(SessionManager sessionManager, MemoryConstrainedCacheManager memoryConstrainedCacheManager) {    DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();    dwsm.setSessionManager(sessionManager);    dwsm.setCacheManager(memoryConstrainedCacheManager);    dwsm.setAuthenticator(modularRealmAuthenticator());    return dwsm;}
复制代码

重写 ModularRealmAuthenticator,只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息。

@Beanpublic ModularRealmAuthenticator modularRealmAuthenticator() {    UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();    modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());    return modularRealmAuthenticator;}
复制代码

①. 构建 ShiroFilterFactoryBean 对象,用于创建过滤工厂

@Beanpublic ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager sessionManager) {//构建ShiroFilterFactoryBean对象,负责创建过滤器工厂    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();//设置登录路径    shiroFilterFactoryBean.setLoginUrl("/login");    //注意:必须设置SecuritManagershiroFilterFactoryBean.setSecurityManager(sessionManager);//设置访问未授权的需要跳转到的路径    shiroFilterFactoryBean.setUnauthorizedUrl("/403");//设置登录成功访问路径    shiroFilterFactoryBean.setSuccessUrl("/");//自定义的过滤设置注入到shiroFilter中    shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter());    shiroFilterFactoryBean.getFilters().put("csrf", new CsrfFilter());    shiroFilterFactoryBean.getFilters().put("user", new UserAuthcFilter());//定义map指定请求过滤规则    Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();    ShiroUtils.loadBaseFilterChain(filterChainDefinitionMap);    ShiroUtils.ignoreCsrfFilter(filterChainDefinitionMap);    filterChainDefinitionMap.put("/**", "apikey, csrf, authc");    return shiroFilterFactoryBean;}
复制代码

Shiro 有两种方式可进行精度控制,一种是过滤器方式,根据访问的 URL 进行控制,该种方式允许使用*匹配 URL,可以进行粗粒度控制;另一种是注解的方式,实现细粒度控制,但只能是在方法上控制,无法控制类级别访问。本文将使用第一种方式编写过滤器文件。

过滤器的类型有很多,本文代码只用到 anon 和 authc 两种类型。

定义一个 Map 类型的 filterChainDefinitionMap,使用 ShiroFilterChainDefinition 来控制请求路径的鉴权与授权。

创建 ShiroUtils 类,自定义静态方法 loadBaseFilterChain()和 ignoreCsrfFilter()方法,判断哪些请求路径需要用户登录才能访问,哪些不需要登录就能访问,实现粗粒度控制。

关键代码(节选):

public static void loadBaseFilterChain(Map<String, String> filterChainDefinitionMap){        filterChainDefinitionMap.put("/resource/**", "anon");        filterChainDefinitionMap.put("/*.worker.js", "anon");        filterChainDefinitionMap.put("/login", "anon");        filterChainDefinitionMap.put("/signin", "anon");}
复制代码

IgnoreCsrfFilter()方法定义的是 authc 类型的过滤设置,authc 表示只有登录后才有权限访问。

public static void ignoreCsrfFilter(Map<String, String> filterChainDefinitionMap) {    filterChainDefinitionMap.put("/", "apikey, authc"); // 跳转到 / 不用校验 csrf    filterChainDefinitionMap.put("/language", "apikey, authc");// 跳转到 /language 不用校验 csrf    filterChainDefinitionMap.put("/test/case/file/preview/**", "apikey, authc"); // 预览测试用例附件 不用校验 csrf}@EventListenerpublic void handleContextRefresh(ContextRefreshedEvent event) {    ApplicationContext context = event.getApplicationContext();    List<Realm> realmList = new ArrayList<>();    LocalRealm localRealm = context.getBean(LocalRealm.class);    LdapRealm ldapRealm = context.getBean(LdapRealm.class);    realmList.add(localRealm);    realmList.add(ldapRealm);context.getBean(DefaultWebSecurityManager.class).setRealms(realmList);}
复制代码

4. 自定义 LdapRealm

Realm 可由 Shiro 提供,也可以自定义。自定义 Realm 一般继承 AuthorizingRealm,然后实现 getAuthenticationInfo()和 getAuthorizationInfo()方法,来完成身份认证和权限获取。

/** * 登录认证 */@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {//构造一个UsernamePasswordToken    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;    String userId = token.getUsername();    String password = String.valueOf(token.getPassword());    return loginLdapMode(userId, password);}
复制代码

在 loginLdapMode()方法中,通过传过来的 userId 调用 userService 里的方法获取 user,然后对 user 进行判断,若通过验证,返回一个 AuthenticationInfo 实现。

private AuthenticationInfo loginLdapMode(String userId, String password) {    String email = (String) SecurityUtils.getSubject().getSession().getAttribute("email");    UserDTO user = userService.getLoginUser(userId, Arrays.asList(UserSource.LDAP.name(), UserSource.LOCAL.name()));    if (user == null) {        user = userService.getUserDTOByEmail(email, UserSource.LDAP.name(), UserSource.LOCAL.name());        if (user == null) {            throw new UnknownAccountException(Translator.get("user_not_exist") + userId);        }        userId = user.getId();    }SessionUser sessionUser = SessionUser.fromUser(user);    SessionUtils.putUser(sessionUser);    return new SimpleAuthenticationInfo(userId, password, getName());}
复制代码

doGetAuthorizationInfo()则用于获取权限相关信息,PrincipalCollection 是一个身份集合。首先通过 getPrimaryPrincipal()得到传入的用户名,然后调用 getAuthorizationInfo()方法,再根据用户名调用 UserService 接口获取角色及权限信息,并将得到的用户 roles 放到 authorizationInfo 中,并返回。

/** * 授权 */@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {    String userId = (String) principals.getPrimaryPrincipal();    return getAuthorizationInfo(userId, userService);}public static AuthorizationInfo getAuthorizationInfo(String userId, UserService userService) {    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();    UserDTO userDTO = userService.getUserDTO(userId);    Set<String> roles = userDTO.getRoles().stream().map(Role::getId).collect(Collectors.toSet());    authorizationInfo.setRoles(roles);    return authorizationInfo;}
复制代码

应用案例-登录认证

1. 流程分析

结合上面 Shiro 框架在 Spring Boot 中关键配置,梳理了一下登录认证的流程分析图。

客户端提交用户账号和密码,在 Controller 中拿到账号和密码封装到 token 对象,然后借助 subject 的 login 方法,把数据提交给 SecurityManager,使用 Authenticator 处理 token,Authenticator 从 Realm 列表中获取 LdapRealm,LdapRealm 从 token 中获取数据,交给 authenticate 进行比对,对比通过返回 AuthenticationInfo。

2. 登录实现

@PostMapping(value = "/signin")public ResultHolder login(@RequestBody LoginRequest request) {    SessionUser sessionUser = SessionUtils.getUser();    if (sessionUser != null) {        if (!StringUtils.equals(sessionUser.getId(), request.getUsername())) {            return ResultHolder.error(Translator.get("please_logout_current_user"));        }    }   SecurityUtils.getSubject().getSession().setAttribute("authenticate", UserSource.LOCAL.name());    return userService.login(request);}
复制代码

在 login 方法中,把用户名和密码封装为 UsernamePasswordToken 对象 token,然后通过 SecurityUtils.getSubject()获取 Subject 对象,并将前面获取 token 对象作为参数。若调用 subject.login(token)时不抛出任何异常,说明认证通过,调用 subject.isAuthenticated()返回 true 表示当前的用户已经登录。后续可以根据 subject 实例获取用户信息。

public ResultHolder login(LoginRequest request) {        String login = (String) SecurityUtils.getSubject().getSession().getAttribute("authenticate");        String username = StringUtils.trim(request.getUsername());        String password = "";        if (!StringUtils.equals(login, UserSource.LDAP.name())) {            password = StringUtils.trim(request.getPassword());            ……        }        UsernamePasswordToken token = new UsernamePasswordToken (username, password, login);        Subject subject = SecurityUtils.getSubject();        try {            subject.login(token);            if (subject.isAuthenticated()) {                UserDTO user = (UserDTO) subject.getSession().getAttribute(ATTR_USER);               ……                                return ResultHolder.success(subject.getSession().getAttribute("user"));} else {        return ResultHolder.error(Translator.get("login_fail"));    }} catch (ExcessiveAttemptsException e) {    throw new ExcessiveAttemptsException(Translator.get("excessive_attempts"));}……}
复制代码

总结

Apache Shiro 是一个功能强大且灵活的开源安全框架,它可以很好地处理身份认证、授权、企业会话管理等,简单易用,可以使项目的验证架构更加完善。本文演示 Spring Boot 集成 Shiro 框架,从身份认证和授权的配置情况进行说明,并演示了基础的身份验证功能,如有不足,请多指教。

更多学习资料戳下方!!!

https://qrcode.ceba.ceshiren.com/link?name=article&project_id=qrcode&from=infoQ&timestamp=1662366626&author=xueqi

用户头像

社区:ceshiren.com 2022.08.29 加入

微信公众号:霍格沃兹测试开发 提供性能测试、自动化测试、测试开发等资料、实事更新一线互联网大厂测试岗位内推需求,共享测试行业动态及资讯,更可零距离接触众多业内大佬

评论

发布
暂无评论
浅谈Shiro框架在Spring Boot中的认证应用_测试_测吧(北京)科技有限公司_InfoQ写作社区