apache shiro是灵活可用的安全框架,本篇文章的Shiro基本配置代码已传至github,欢迎点击获取,您可以直接在此基础上进行项目的二次开发。
1. 环境准备
首先,先创建springboot项目(这里我的版本号采用的是spring boot2.0),选择引入web、mysql、mybatis的依赖
创建完成后再手动引入shiro、druid连接池、工具包、jsp等的依赖
Apache Shiro不会去维护用户、维护权限;这些需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 即可。我们需要设计一套用户权限的相关体系,这里基于RBAC模型设计出User类、Role类和Permission类(省略getter/setter)。
public class User {
private Integer uid;
private String username;;
private String password;
private Set<Role> roles = new HashSet<>();
}
public class Role {
private Integer rid;
private String rname;
private Set<Permission> permissions = new HashSet<>();
private Set<User> users = new HashSet<>();
}
public class Permission {
private Integer pid;
private String name;
private String url;
}
然后创建好test数据库、user、role和permission及其关联表,并创建service层和dao层,因内容过多,这里不展示了,请直接查看源码。
2. 配置Shiro
完成上面准备后,现在真正开始Shiro的环境搭建。首先,创建一个AuthRealm类并继承AuthorizingRealm。
这里介绍下:
Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
下面是AuthRealm类的实现代码:
public class AuthRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;
// principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。
// 一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();
List<String> permissionList = new ArrayList<>();
List<String> roleList = new ArrayList<>();
Set<Role> roleSet = user.getRoles();
if (!CollectionUtils.isEmpty(roleSet)) {
roleSet.forEach(role -> {
roleList.add(role.getRname());
Set<Permission> permissionSet = role.getPermissions();
if (!CollectionUtils.isEmpty(permissionSet)) {
permissionList.addAll(permissionSet.stream()
.map(permission -> permission.getName())
.collect(Collectors.toList()));
}
});
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roleList);
info.addStringPermissions(permissionList);
return info;
}
/**
* Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
* 登录时调用subject.login(token),Subject会委托SecuriyManager执行,SecuriyManager调用它的Realm执行会跳转到这里,
* 本例中 /loginUser 接口创建的UsernamePasswordToken会传到这里。
*
* 执行流程:
* 首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException;
* 如果user找到但锁定了抛出锁定异常LockedAccountException;
* 最后生成AuthenticationInfo信息,交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,
* 如果不匹配将抛出密码错误异常IncorrectCredentialsException;
* 另外如果密码重试此处太多将抛出超出重试次数异常ExcessiveAttemptsException;
* 在组装SimpleAuthenticationInfo信息时,需要传入:身份信息(用户名)、凭据(密文密码)、盐(username+salt),
* CredentialsMatcher使用盐加密传入的明文密码和此处的密文密码进行匹配。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
User user = userService.findByUsername(username);
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
}
}
然后,我们创建一个CredentialMatcher类继承SimpleCredentialsMatcher,这是用于密码校验的。
代码实现如下:
public class CredentialMatcher extends SimpleCredentialsMatcher {
// 匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密)
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String password = new String (usernamePasswordToken.getPassword());
String dbPassword = (String) info.getCredentials();
return this.equals(password, dbPassword);
}
}
最后,我们创建ShiroConfiguration类,用来集成管理Shiro的所有配置。其中包括将上面创建的CredentialMatcher密码类设置到AuthRealm类中,将AuthRealm类设置到SecuriryManager类中,使用Shiro的内置过滤器,shiro和Spring关联,开启AOP代理等。
代码实现如下:
@Configuration
public class ShiroConfiguration {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(manager);
// 登录接口
bean.setLoginUrl("/login");
// 验证成功接口
bean.setSuccessUrl("/index");
// 未验证接口
bean.setUnauthorizedUrl("/unauthorized");
// public enum DefaultFilter {...}
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 【authc】:是org.apache.shiro.web.filter.authc.FormAuthenticationFilter类型的实例,其用于实现基于表单的身份验证
filterChainDefinitionMap.put("/index", "authc");
// 【anon】:表示不需要登录即可访问
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/loginUser", "anon");
// 【"/admin", "roles[admin]"】:表示只有角色为admin的用户可以访问/admin接口
filterChainDefinitionMap.put("/admin", "roles[admin]");
// 【"/edit", "perms[edit]"】:表示拥有edit权限才能访问/edit接口
filterChainDefinitionMap.put("/edit", "perms[edit]");
// **:匹配路径中的零个或多个路径,如/admin/**将匹配/admin/a或/admin/a/b。
// 【"/druid/**", "anon"】:表示不拦截访问/druid/下的任意请求
filterChainDefinitionMap.put("/druid/**", "anon");
// 【user】:认证过滤器,表示必须存在用户,当登入操作时不做检查
filterChainDefinitionMap.put("/**", "user");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
// SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且管理着所有 Subject;
// 它是 Shiro 的核心,负责与后边介绍的其他组件进行交互,类似于SpringMVC 中的 DispatcherServlet 前端控制器;
@Bean("securityManager")
public SecurityManager securityManager(AuthRealm authRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(authRealm);
return manager;
}
// Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,
// 那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限
// 进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
@Bean("authRealm")
public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher credentialMatcher) {
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(credentialMatcher);
return authRealm;
}
// 自定义的密码校验
@Bean("credentialMatcher")
public CredentialMatcher credentialMatcher() {
return new CredentialMatcher();
}
// 用于开启Shiro Spring AOP权限注解的支持
// 处理shiro和spring关联,让spring管理shiro时使用我们自定义的SecurityManager
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(manager);
return advisor;
}
// 处理shiro和spring关联,使用AOP代理类
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
3. 测试
现在我们创建一个Controller类,提供ShiroConfiguration配置类中注入过滤器配置的接口【相关的JSP请查看源码获取】。
代码实现如下:
@Controller
public class TestController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/index")
public String index() {
return "index";
}
@GetMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
subject.logout();
}
return "login";
}
@RequestMapping("/unauthorized")
public String unauthorized() {
return "unauthorized";
}
@RequestMapping("/edit")
@ResponseBody
public String edit() {
return "edit success";
}
@GetMapping("/admin")
@ResponseBody
public String admin() {
return "admin success";
}
@PostMapping("/loginUser")
public String loginUser(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpSession session) {
// 创建用户名/密码身份验证Token
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,
// 如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互
// 都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
User user = (User) subject.getPrincipal();
session.setAttribute("user", user);
return "index";
} catch (Exception e) {
return "login";
}
}
}
4. 最后
感谢您阅读我的文章,文笔不好,能力有限,以上表达不当的地方还望指出。