通常,公司的项目都会有严格的认证和授权操作,在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版本号
在pom.xml中可以使用properties标签来处理版本号,在标签内将版本号作为变量进行声明,在后面dependency中用到版本号时通过${变量名}去获取相关版本号。当版本号发生变化的时候,只修改properties中的相关版本号变量就可以,不用更新所有依赖的版本号。
<properties> <shiro.version>1.6.0</shiro.version> <java.version>1.8</java.version> <jmeter.version>5.4.1</jmeter.version> </properties>
2. 添加依赖,并通过${shiro.version}获取shiro的版本
<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身份验证成功的认证信息。
@Bean public ModularRealmAuthenticator modularRealmAuthenticator() { UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator(); modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy()); return modularRealmAuthenticator; }
3.3添加ShiroFilterFactoryBean对象的配置
①. 构建ShiroFilterFactoryBean对象,用于创建过滤工厂
代码如下:
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager sessionManager) { //构建ShiroFilterFactoryBean对象,负责创建过滤器工厂 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置登录路径 shiroFilterFactoryBean.setLoginUrl("/login"); //注意:必须设置SecuritManager shiroFilterFactoryBean.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()方法,判断哪些请求路径需要用户登录才能访问,哪些不需要登录就能访问,实现粗粒度控制。
关键代码(节选):
loadBaseFilterChain()方法定义的是anon类型的过滤设置,anon表示没有登录也有权限访问。
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 }
3.4 securityManager不用直接注入Realm,可能导致事务失效
可以定义一个handleContextRefresh方法,利用监听去初始化,等到ApplicationContext 加载完成之后 装配shiroRealm。
@EventListener public 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()方法,来完成身份认证和权限获取。
getAuthenticationInfo() 方法,用于验证账户和密码,并返回相关信息。这里是将用户名和密码作为参数,调用loginLdapMode()方法去完成身份认证。
/** * 登录认证 */ @Override protected 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中,并返回。
/** * 授权 */ @Override protected 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. 登录实现
客户端发送post请求,首先对请求体中的用户数据与session中的数据作对比,然后通过SecurityUtils.getSubject()设置属性authenticate为UserSource.LOCAL.name(),最后调用UserService中自定义的login()方法,并将请求体作为参数。
@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框架,从身份认证和授权的配置情况进行说明,并演示了基础的身份验证功能,如有不足,请多指教。