maven方式在项目pom.xml中引入shiro starter包的坐标,这里引用了1.4.1版本
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.1</version>
</dependency>
starter已经做了很多自动配置工作,具体可以参考ShiroAutoConfiguration.java、ShiroBeanAutoConfiguration.java和ShiroWebAutoConfiguration.java这几个文件。
这里使用新建shiroConfig.java类方式进行shiro配置。主要配置Realm、url过滤器、密码匹配器和安全管理器这几个组件就可以让shiro正常工作。
@Configuration
public class shiroConfig {
// 配置自定义Realm
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(credentialsMatcher()); //配置使用哈希密码匹配
return userRealm;
}
// 配置url过滤器
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/captcha", "anon");
chainDefinition.addPathDefinition("/logout","anon");
chainDefinition.addPathDefinition("/layuiadmin/**", "anon");
chainDefinition.addPathDefinition("/druid/**", "anon");
chainDefinition.addPathDefinition("/api/**", "anon");
// all other paths require a logged in user
chainDefinition.addPathDefinition("/login","anon");
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
// 设置用于匹配密码的CredentialsMatcher
@Bean
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); // 散列算法,这里使用更安全的sha256算法
credentialsMatcher.setStoredCredentialsHexEncoded(false); // 数据库存储的密码字段使用HEX还是BASE64方式加密
credentialsMatcher.setHashIterations(1024); // 散列迭代次数
return credentialsMatcher;
}
// 配置security并设置userReaml,避免xxxx required a bean named 'authorizer' that could not be found.的报错
@Bean
public SessionsSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
return securityManager;
}
}
shiro默认的登录页是/login.jsp,需要在项目配置文件application.yml中修改默认登录页等配置。
shiro:
loginUrl: /login
successUrl: /
shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置过滤指定url的访问权限。
配置缩写 | 对应的过滤器 | 功能 |
---|---|---|
anon | AnonymousFilter | 指定url可以匿名访问 |
authc | FormAuthenticationFilter | 指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。 |
authcBasic | BasicHttpAuthenticationFilter | 指定url需要basic登录 |
logout | LogoutFilter | 登出过滤器,配置指定url就可以实现退出功能,非常方便 |
noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
port | PortFilter | 需要指定端口才能访问 |
rest | HttpMethodPermissionFilter | 将http请求方法转化成相应的动词来构造一个权限字符串,这个感觉意义不大,有兴趣自己看源码的注释 |
roles | RolesAuthorizationFilter | 需要指定角色才能访问 |
ssl | SslFilter | 需要https请求才能访问 |
user | UserFilter | 需要已登录或“记住我”的用户才能访问 |
shiro常用的权限控制注解,可以在控制器类上使用
注解 | 功能 |
---|---|
@RequiresGuest | 只有游客可以访问 |
@RequiresAuthentication | 需要登录才能访问 |
@RequiresUser | 已登录的用户或“记住我”的用户能访问 |
@RequiresRoles | 已登录的用户需具有指定的角色才能访问 |
@RequiresPermissions | 已登录的用户需具有指定的权限才能访问 |
Realm是实现自定义登录和授权的核心类,这里继承了抽象类AuthorizingRealm并重写doGetAuthenticationInfo(用于登录验证)和doGetAuthorizationInfo(用于权限验证)这俩个方法。doGetAuthenticationInfo主要作用是获取用户输入的用户名、密码等信息并从数据库中取出保存的密码交给shiro,由shiro的密码匹配器进行匹配。
/**
* 认证 ,用户名密码校验
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
User user = userDao.getByUsername(username);
if (user == null) {
throw new UnknownAccountException(); // 账号不存在
}
if (user.getStatus() != 0) {
throw new LockedAccountException(); // 账号被锁定
}
String salt = user.getSalt();
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,
user.getPassword(),
ByteSource.Util.bytes(salt),
getName());
return authenticationInfo;
}
根据用户输入的用户名,在数据库中查找到用户记录,并用查到的用户对象、数据库中存储的密码、密码盐和Realm对象名字构建一个认证信息对象(SimpleAuthenticationInfo)交给系统进行密码验证。
doGetAuthorizationInfo主要是获取用户的角色和权限,并交给Shiro去判断是否具有访问资源的权限。
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 角色
Set<String> roles = new HashSet<>();
// 权限
// 测试用权限
if ("admin".equals(user.getUsername())) {
roles.add("admin");
permissions.add("op:write");
} else {
roles.add("user");
permissions.add("op:read");
}
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
支持shiro的配置工作已经基本完成,已经可以正常工作了。接下来就是与前端配合完成页面的登录和权限控制等工作。
在需要登录的前端 html页面上,向后台登录url提交包含用户名和密码字段的表单。
在后台登录url中,接收用户名密码,据此创建一个usernamePasswordToken令牌,交由Shiro并调用login()方法进行登录,如果不抛出任何异常表明登录成功,如果抛出异常,这根据异常种类返回提示出错信息给用户。
@ResponseBody
@PostMapping("login")
public JsonResult doLogin(String username, String password, String vercode, HttpServletRequest request) {
if ("".equals(username.trim()) || "".equals(password.trim())) {
return JsonResult.error("账号或密码不能为空");
}
if (!CaptchaUtil.ver(vercode, request)) {
//CaptchaUtil.clear(request); // 清除session中的验证码
return JsonResult.error("验证码不正确");
}
try {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
SecurityUtils.getSubject().login(token);
//addLoginRecord(getLoginUserId(), request); // 记录登录信息
HashMap<String, String> map = new HashMap<>();
map.put("access_token", "1111111111111111111"); // 模拟登录令牌
return JsonResult.ok("登录成功").put("data", map);
} catch (IncorrectCredentialsException ice) {
return JsonResult.error("密码错误");
} catch (UnknownAccountException uae) {
return JsonResult.error("账号不存在");
} catch (LockedAccountException e) {
return JsonResult.error("账号被锁定");
} catch (ExcessiveAttemptsException eae) {
return JsonResult.error("操作频繁,请稍后再试");
}
}
验证码使用的是 EasyCaptcha工具
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
我们在配置中使用的是HashedCredentialsMatcher匹配器,使用更安全的sha256哈希算法,指定了数据库中密码字段使用base64方式加密。
那么在创建用户或修改密码时怎么生成加密密码呢?利用shiro提供的simplehash()方法就可以,如下指定了sha256算法,密码字符串,密码盐和迭代次数(需要和config配置里的次数相同),最后对生成的哈希密码串进行base64编码。
new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, ByteSource.Util.bytes(salt), hashIterations).toBase64();
shiro可以使用url配置控制权限,也可以在控制器类上使用注解控制权限。同时使用两种配置方式灵活结合,才是适应不同应用场景的最佳实践。只用注解或只用url配置,都不够灵活,有时会很麻烦。思路是:
用url配置控制鉴权,实现粗粒度控制;用注解控制授权,实现细粒度控制
注意:解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射,需加入以下配置:
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
//defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
控制器上通过注解配置详细的角色和权限,多个权限和角色之间默认是“与”关系,可以通过logical参数设置为“或”。
@RequiresRoles("user")
@GetMapping("/user/list.html")
public String userList() {
return "user/user/list";
}
@RequiresPermissions("op:read")
@GetMapping("user/userform.html")
public String userForm() {
return "user/user/userform";
}
@RequiresRoles("admin")
@GetMapping("/administrators/list.html")
public String adminList() {
return "user/administrators/list";
}
@RequiresPermissions("op:write")
@GetMapping("administrators/adminform.html")
public String administratorForm() {
return "user/administrators/adminform";
}
@RequiresRoles( value = {"admin", "user"}, logical = Logical.OR)
@GetMapping("/administrators/role.html")
public String roleList() {
return "user/administrators/role";
}