shiro作为安全框架,使用还是比较多,而且相对来说,比较容易上手。下面将使用springboot来整合shiro,实现的功能如下:
(1)实现访问控制,未登录时,只能访问登录接口
(2)实现角色和权限的访问控制
示例代码,已放在了GitHub上:https://github.com/qiuxinfa/shiro-study
先看下目录结构:
1.maven依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
2.创建一个用户类User:
public class User {
private String username;
private String password;
private Integer enable; //账号是否锁定
//省略set、get、toString方法
}
3.创建一个业务查询类,模拟数据存储和数据查询功能,因为这里没有连接数据库:
@Service
public class UserService {
//用来存储用户信息的map
private Map<String,User> userInfo = new HashMap<>(3);
public UserService(){
//用户信息
userInfo.put("admin",new User("admin","123",1));
userInfo.put("sam",new User("sam","123",1));
userInfo.put("qxf",new User("qxf","123",0));
}
//根据用户名,查找用户,模拟数据库查询
public User getUserByUsername(String username){
return userInfo.get(username);
}
//获取所有的用户
public Map<String,User> getAllUser(){
return userInfo;
}
}
这里为了简单起见,用一个map来存储用户信息,实际使用中,用户信息肯定是存储在数据库中的。
4.自定义身份认证和授权的realm,继承了AuthorizingRealm ,重写认证和授权方法:
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User)SecurityUtils.getSubject().getPrincipal(); //获取当前登录的用户信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//这里模拟数据库,进行角色和权限的处理
//这里只是简单模拟,角色使用是用户名,权限是包含用户名的路径
simpleAuthorizationInfo.addRole(user.getUsername());
simpleAuthorizationInfo.addStringPermission("/"+user.getUsername());
return simpleAuthorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
//这里模拟数据库查询用户,根据用户名查询
User dbUser = userService.getUserByUsername(username);
if (dbUser == null){
//账号不存在
throw new UnknownAccountException();
}
if (dbUser.getEnable()==0){
//账号被锁定
throw new LockedAccountException();
}
return new SimpleAuthenticationInfo(dbUser, dbUser.getPassword(), getName());
}
}
5.配置shiro
/**
*shiro配置
*/
@Configuration
public class ShiroConfig {
@Bean
MyRealm myRealm() {
//返回自定义的身份认证和授权realm
return new MyRealm();
}
@Bean
SecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
return manager;
}
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
//设置登录的url
bean.setLoginUrl("/login");
Map<String, String> map = new LinkedHashMap<>();
//anon表示,匿名访问,不需要认证就可以访问的接口
map.put("/doLogin", "anon");
//authc表示需要身份认证后才可以访问
map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
//下面配置的3个bean,主要是为了Shiro的注解(如@RequiresRoles,@RequiresPermissions)生效
@Bean
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
这里需要注意一下,如果想要使用shiro的注解生效,需要这3个bean:
(1)LifecycleBeanPostProcessor
(2)DefaultAdvisorAutoProxyCreator
(3)AuthorizationAttributeSourceAdvisor
6.定义访问接口:
@Controller
public class ShiroController {
@GetMapping("/login")
public String login(){
return "login.html";
}
@PostMapping("/doLogin")
@ResponseBody
public String doLogin(String username,String password){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return "登录成功";
}catch (UnknownAccountException|IncorrectCredentialsException e){
e.printStackTrace();
return "账号或密码错误";
}catch (LockedAccountException e){
e.printStackTrace();
return "账号已被锁定,请联系管理员";
}catch (AuthenticationException e){
e.printStackTrace();
return "未知异常,请联系管理员";
}
}
@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello:"+SecurityUtils.getSubject().getPrincipal();
}
}
这里定义了3个接口:
(1)login,处理get请求,匿名可访问,会跳转到login.html登录页面
(2)doLogin,处理post请求,实现真正的登录功能
(3)hello,获取自身信息,认证后才可以访问
7.登录页面,写得很简陋,就是一个简单HTML的form表单提交:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/doLogin" method="post">
用户名:<input type="text" name="username" />
<br/>
密 码:<input type="password" name="password"/>
<br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
8.测试
(1)验证这个功能:未登录时,只能访问登录接口
写个启动类,启动项目后,在浏览器输入http://localhost:8080/hello,则会跳转到登录页面,因为在UserService里面添加了3个用户:
//用户信息
userInfo.put("admin",new User("admin","123",1));
userInfo.put("sam",new User("sam","123",1));
userInfo.put("qxf",new User("qxf","123",0));
所以,当我们使用admin或者sam登录时(密码都是123),就可以登录成功(qxf账号是被锁定了),登录成功之后,再访问hello就可以看到自己的信息了。
(2)验证角色和权限访问控制:
在MyRealm的授权方法中,我是用 用户名作为角色添加了,用 /+用户名作为权限,真实项目肯定不是这样的,一般会配置在数据库中,这里只是为了简单起见:
//这里模拟数据库,进行角色和权限的处理
//这里只是简单模拟,角色使用是用户名,权限是包含用户名的路径
simpleAuthorizationInfo.addRole(user.getUsername());
simpleAuthorizationInfo.addStringPermission("/"+user.getUsername());
添加只有admin可以访问的接口:
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private UserService userService;
@GetMapping("/allUser")
@RequiresRoles("admin") //表示需要有[admin]这个角色才可以访问
@RequiresPermissions("/admin") //表示需要有 /admin 权限才可以访问
public Collection<User> allUser(){
List<User> list = new ArrayList<>();
Map<String,User> userMap = userService.getAllUser();
return userMap.values();
}
}
再写一个全局异常处理,用以捕获没有权限访问的异常:
/**
* @Auther: qiuxinfa
* @Date: 2020/4/2
* @Description: 定义全局异常处理
*/
@RestControllerAdvice
public class MyGlobalException {
//表示只捕获AuthorizationException类及其子类异常
@ExceptionHandler(AuthorizationException.class)
public String handlerAuthorizationException(AuthorizationException e){
e.printStackTrace();
return "你没有权限访问";
}
}
现在,可以启动项目了,如果用sam登录后,访问http://localhost:8080/admin/allUser,那么就会返回如下信息:
你没有权限访问
后台也会打印出,需要角色admin才可以访问,其实还需要/admin权限才可以访问
如果用admin登录后,访问http://localhost:8080/admin/allUser,则会返回所有的用户信息:
[{"username":"admin","password":"123","enable":1},{"username":"sam","password":"123","enable":1},{"username":"qxf","password":"123","enable":0}]
这样就实现了角色的访问控制,权限也是一样的道理,为了简单起见,demo中尽量返回的是json,只有一个登录页面。
其他注意点:
授权不是认证之后就立马执行的,而是当你访问某个资源需要权限时,才会执行授权的方法,比如访问添加了
@RequiresRoles,RequiresPermissions注解的时候,才会触发进入到我们自定义realm的授权方法。