最近开发个人博客,主要内容部分已经完成,就到了设计权限的部分,于是把shiro翻出来复习了一下,现在记录一下测试过程:
0. shiro逻辑
参考https://www.jianshu.com/p/7464327c83fe
//创建一个默认SecurityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//创建一个自定义Realm对象
CustomRealm realm = new CustomRealm();
//将自定义Realm注入到SecurityManager里
defaultSecurityManager.setRealm(realm);
//创建加密Matcher,加密方式为md5,加密次数1
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");
matcher.setHashIterations(1);
//注入到Realm中,这样在验证的时候,会把token传进去的密码自动加密
realm.setCredentialsMatcher(matcher);
//获取Subject
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
//设置Token
AuthenticationToken token = new UsernamePasswordToken("Hiway","123456");
//登录验证
subject.login(token);
//是否成功验证
System.out.println("isAuthentication:"+subject.isAuthenticated());
//是否拥有角色
subject.checkRoles("admin");
//是否拥有权限
subject.checkPermission("user:delete");
//登出
subject.logou();
shiro的主要模块:
1. 导入依赖
<!--导入shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
2. 配置ShiroConfig
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(){
ShiroFilterFactoryBean shiroFilterFactoryBean= new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(getSecurityManager());
//设置登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
Map<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
//先测一个queryAll的授权
//"anon"可以匿名登录,比如filterChainDefinitionMap.put("/index","anon");
//"perms"授权 比如filterChainDefinitionMap.put("/api/queryAll","perms[user:form]");
//表示必须有user:form权限
filterChainDefinitionMap.put("/api/queryAll","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//开启shiro的注解 这部份是thymeleaf用的
// @Bean(name = "shiroDialect")
//public ShiroDialect shiroDialect() {
// return new ShiroDialect();
//}
@Bean
public SecurityManager getSecurityManager(){
DefaultSecurityManager defaultSecurityManager=new DefaultWebSecurityManager();
//将自定义的customRealm放入DefaultSecurityManager中
defaultSecurityManager.setRealm(customRealm());
return defaultSecurityManager;
}
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm = new CustomRealm();
//自定义加密类new MyCredentialsMatcher()
customRealm.setCredentialsMatcher(new MyCredentialsMatcher());
return customRealm;
}
/**
* *
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* *
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* * @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(getSecurityManager());
return authorizationAttributeSourceAdvisor;
}
}
3. 自定义加密类MyCredentialsMatcher()
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String userName = (String) token.getPrincipal();
String userPwd = new String((char[]) token.getCredentials());
//MD5加盐,并迭代两次
String md5Pwd = new SimpleHash("MD5", userPwd,
ByteSource.Util.bytes(userName), 2).toHex();
String accountCredentials = (String) getCredentials(info);
return super.equals(md5Pwd, accountCredentials);
}
}
4. 自定义CustomRealm
public class CustomRealm extends AuthorizingRealm {
//授权
@Override
public AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("=====授权认证======");
//获取用户名
String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//通过username从数据库中查询权限信息,这里为了测试就直接写死,不查了!
Set<String> stringSet = new HashSet<>();
//增加两个授权
stringSet.add("user:show");
stringSet.add("user:admin");
info.setStringPermissions(stringSet);
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("=====身份认证======");
//通过令牌拿到userName和userPwd
String userName = (String) authenticationToken.getPrincipal();
String userPwd = new String((char[]) authenticationToken.getCredentials());
//根据用户名从数据库获取密码,加密方法见CustomRealm的CredentialsMatcher属性设置
//查询数据库,判断该用户是否存在,这里为了测试也不查了,写死。就是用户名:xinxin,密码:123456
//自定义加密后的值:4a64f6bf6c50a9fc822e0bec4248c818
String password = "4a64f6bf6c50a9fc822e0bec4248c818";
if (StringUtils.isEmpty(userName)) {
throw new UnknownAccountException("用户名为空");
}
return new SimpleAuthenticationInfo(userName, password, getName());
}
5. Controller层
//登录
@PostMapping("/login")
public Object toLogin(@RequestParam("username") String username, @RequestParam("password") String password) {
//从SecurityUtils里边创建一个subject
Subject subject = SecurityUtils.getSubject();
//在认证提交前准备token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//自定义返加的token,登录后自动分配一个线程Id
String token_res = subject.getSession().getId().toString();
Map<String,Object> map=new HashMap<>();
map.put("token",token_res);
//执行认证登陆,抛出的异常在GlobalExceptionHandler中处理
subject.login(token);
if (subject.isAuthenticated()) {
return ApiResult.succ(map);
} else {
token.clear();
return new ResponseEntity<>("登录失败", HttpStatus.NOT_FOUND);
}
}
//增加一个授权"user:list"
@RequiresPermissions("user:list")
@GetMapping("/api/queryAll")
public ResponseEntity<Object> queryAll(){
//具体articleService的接口实现,就不在此赘述了,可自行实现
List<Article> articles = articleService.queryAll();
return new ResponseEntity<>(articles, HttpStatus.OK);
}
6. 统一异常处理
我将所有shiro异常都放在GlobalExceptionHandler中
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//省略之前定义的其他异常
//======================处理shiro的Exception start======================================================
/**
* 处理 IncorrectCredentialsException
*/
@ExceptionHandler(value = IncorrectCredentialsException.class)
public ResponseEntity<String> incorrectCredentialsException(IncorrectCredentialsException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return new ResponseEntity<>("密码错误", HttpStatus.NOT_FOUND);
}
/**
* 处理 IncorrectCredentialsException
*/
@ExceptionHandler(value = LockedAccountException.class)
public ResponseEntity<String> lockedAccountException(LockedAccountException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return new ResponseEntity<>("账户已锁定", HttpStatus.NOT_FOUND);
}
/**
* 处理 IncorrectCredentialsException
*/
@ExceptionHandler(value = ExcessiveAttemptsException.class)
public ResponseEntity<String> excessiveAttemptsException(ExcessiveAttemptsException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return new ResponseEntity<>("用户名或密码错误次数过多", HttpStatus.NOT_FOUND);
}
/**
* 处理 IncorrectCredentialsException
*/
@ExceptionHandler(value = AuthenticationException.class)
public ResponseEntity<String> authenticationException(AuthenticationException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return new ResponseEntity<>("用户名或密码不正确", HttpStatus.NOT_FOUND);
}
}
//======================处理shiro请求的Exception end======================================================
shiro的自定义异常有哪些,可以在IDEA中查看源码AuthenticationException的子类:
7. 测试
用Postman进行测试,首先是登录
返回了token。此时username与password都已存入session中,再访问”http://127.0.0.1:4000/api/queryAll“
由于我们自己增加的是"user:show"与"user:admin",而注解要求的是"user:list",所以不符合。
8. 小结
由于我的个人博客用了redis,后续需要监听shiro中的session处理,把用户名与密码都保存到redis中,让shiro主要从redis中判断用户是否登录,同时定义token过期后,再登录跳转页面返回之前的页面而不是login页面,后续搞定后会上传笔记。shiro学起来还是挺别扭,有些类看起来不在一起,但是通过父类方法联系在一起,而且接口实现类很多,debug起来太不方便,只能一边学习一边做笔记。