通常,公司的项目都会有严格的认证和授权操作,在 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 身份验证成功的认证信息。
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {
UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return modularRealmAuthenticator;
}
复制代码
①. 构建 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()方法,判断哪些请求路径需要用户登录才能访问,哪些不需要登录就能访问,实现粗粒度控制。
关键代码(节选):
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
}
@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()方法,来完成身份认证和权限获取。
/**
* 登录认证
*/
@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. 登录实现
@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×tamp=1662366626&author=xueqi
评论