点击上方蓝字关注我们!
通常,公司的项目都会有严格的认证和授权操作,在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中的相关版本号变量就可以,不用更新所有依赖的版本号。
1.6.0
1.8
5.4.1
2. 添加依赖,并通过${shiro.version}获取shiro的版本
org.apache.shiro
shiro-spring-boot-web-starter
${shiro.version}
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 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 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 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 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 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框架,从身份认证和授权的配置情况进行说明,并演示了基础的身份验证功能,如有不足,请多指教。