Shiro官网
以下为Shiro的简单介绍:
具体的信息可进一步查看Shiro官网
Shiro 有三大核心组件,即 Subject、SecurityManager 和 Realm。它们的关系如下图:
Subject 为认证主体,它包含 Principals 和 Credentials 两个信息。
简单来说:这两者代表了需要认证的内容,最常见的便是用户名、密码了
SecurityManager 为安全管理员,它是Shiro 架构的核心。所有具体的交互都通过SecurityManager进行控制;负责所有Subject、且负责进行认证和授权、及会话、缓存的管理
Realm 是一个域,它是连接 Shiro 和具体应用的桥梁。当需要与安全数据交互时,比如用户账户、访问控制等,Shiro 将会在一个或多个 Realm 中查找。
官方的入门实例:http://shiro.apache.org/tutorial.html
大致代码如下:
shiro.ini
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
ini文件大致意思是:
Java代码
public static void main(String[] args) {
Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();
if ( !currentUser.isAuthenticated() ) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
currentUser.login(token);
}
if ( currentUser.hasRole( "schwartz" ) ) {
log.info("May the Schwartz be with you!" );
} else {
log.info( "Hello, mere mortal." );
}
if ( currentUser.isPermitted( "lightsaber:wield" ) ) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
...
}
上面代码主要有两个步骤:
认证:
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
currentUser.login(token);
判断权限:
currentUser.hasRole( "schwartz" )
IDEA:2018.2(lombok插件)
SpringBoot:2.3.1.RELEASE
Shiro:1.3.2
freemarker(前端页面)
org.springframework.boot
spring-boot-starter-web
org.apache.shiro
shiro-spring
1.3.2
org.springframework.boot
spring-boot-starter-freemarker
org.projectlombok
lombok
1.18.8
provided
根据官方文档 可以直接使用starter依赖包,但按照网上的搭建过程基本上是有坑的。所以,这里就使用了shiro-spring的依赖包咯(可查看这篇博客)。
步骤:
1)、导入依赖包
上述有。
2)、修改配置文件application.yml
spring:
freemarker:
suffix: .ftl
template-loader-path: classpath:/templates/
settings:
#解决前台使用${}赋值值为空的情况
classic_compatible: true
这里就是对前端页面freemarker的配置。至于对shiro的配置,就是通过SpringBoot中的配置类进行的。
3)、配置Shiro
配置shiro的securityManager和自定义realm。因为realm负责我们的认证与授权,所以是必须的,自定义的realm必须要交给securityManager管理,所以这两个类需要重写。然后还有一些资源的权限说明,所以一般需要定义ShiroFilterFactoryBean,所以有3个类我们常写的:
4)、动手编码
ShiroConfig:
@Slf4j
@Configuration
public class ShiroConfig {
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(userRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置登录页面的url;如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/login");
// 设置授权失败时应该跳转的页面(即:访问了权限不足的页面时,需要跳转到/role对应的url)
shiroFilterFactoryBean.setUnauthorizedUrl("/role");
shiroFilterFactoryBean.setSuccessUrl("/success");
// 设置拦截器
Map filterMap = new LinkedHashMap<>();
// anon:表示可以匿名访问(不需要认证)
filterMap.put("/guest/**", "anon");
// 用户,需要角色权限 user
filterMap.put("/user/**", "roles[user]");
filterMap.put("/admin/**", "roles[admin]");
// 开放登陆接口
filterMap.put("/doLogin", "anon");
filterMap.put("/login", "anon");
// 其余接口一律拦截
// 主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
log.info("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
}
【注意】:SecurityManager 类导入的应该是 import org.apache.shiro.mgt.SecurityManager;
shirFilter()方法中主要设置了一些跳转url(如:没有登录时,会跳转到 /login 对应的页面)以及各类url的权限拦截(如:访问"/user"开头的url,需要"user"的角色;访问"/guest"开头的url,可以匿名访问)。
权限拦截 Filter
这里对Shiro中的Filter稍加说明一下:当运行一个Web应用程序时,Shiro将会创建一些有用的默认 Filter 实例,并自动地将它们置为可用,而这些默认的 Filter 实例是被 DefaultFilter 枚举类定义的。
对其中一些过滤器稍加解释一下:
Filter | 解释 |
---|---|
anon | 无参,开放权限,可以理解为匿名用户或游客 |
authc | 无参,需要认证 |
logout | 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url |
user | 无参,表示必须存在用户,当登入操作时不做检查 |
perms[user] | 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算通过 |
roles[admin] | 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles[“admin,user”],当有多个参数时必须每个参数都通过才算通过 |
【注意】:anon, authc, authcBasic, user 是第一组认证过滤器;perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作。即:先要完成认证才能前去寻找授权,才能走第二组授权器。如:访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 shiroFilterFactoryBean.setLoginUrl(); 设置的 url
UserRealm:
要继承 AuthorizingRealm 类来自定义我们自己的 realm 以进行我们自定义的认证和授权操作
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取授权用户信息(principalCollection存放了认证成功的用户信息)
User user = (User) principalCollection.iterator().next();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// TODO 改为从数据库中获取该用户的角色
authorizationInfo.addRole(user.getRole());
return authorizationInfo;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
String username = token.getUsername();
String password = String.valueOf(token.getPassword());
// TODO 改为从数据库获取对应用户名密码的用户
User user = userService.getUserByName(username);
if (null == user) {
throw new UnknownAccountException("用户名不存在");
} else if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("密码不正确");
}
SecurityUtils.getSubject().getSession().setAttribute("user", user);
return new SimpleAuthenticationInfo(user, password, getName());
}
}
重写的两个方法分别是实现身份认证以及授权认证。
UserServiceImpl:
UserServiceImpl是UserService接口的实现类,这里就贴出它的实现类的代码了。
@Service
public class UserServiceImpl implements UserService {
private static Map userMap = new HashMap<>();
static {
userMap.put("zzc", new User("1", "zzc", "666", "user"));
userMap.put("wzc", new User("2", "wzc", "888", "user"));
userMap.put("yht", new User("3", "yht", "999", "admin!"));
}
@Override
public User getUserByName(String username) {
// Map通过key进行得到value
return userMap.get(username);
}
}
这里没使用数据库了,就直接使用了假数据,并存储在了Map中,通过静态代码块添加了部分数据。Map的key是id(这里假设id是与username 一样,方便调用getUserByName()),value是User。
User:
@Data
public class User {
private String id;
private String username;
private String password;
// 角色
private String role;
public User(String id, String username, String password, String role) {
this.id = id;
this.username = username;
this.password = password;
this.role = role;
}
}
好了,接下来就是controller层了。
GuestController:
@RestController
@RequestMapping("/guest")
public class GuestController {
@GetMapping("/enter")
public String enter() {
return "欢迎进入,您的身份是游客";
}
}
UserController:
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/enter")
public String enter() {
return "欢迎进入,您的身份是用户";
}
}
AdminController:
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/enter")
public String enter() {
return "Admin enter!!";
}
}
这上面Controller里面的内容基本一致,就是为了测试不同的url是由不同的角色才能访问的(接下去继续看)。
LoginController:
@Slf4j
@Controller
public class LoginController {
@Autowired
private HttpServletRequest request;
// 进入登录页面
@GetMapping("/login")
public String login() {
log.info("进入login()方法");
return "login";
}
// 执行登录操作
@PostMapping("/doLogin")
public String doLogin(String username, String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
String errorMsg = null;
try {
SecurityUtils.getSubject().login(token);
} catch (AuthenticationException e) {
if (e instanceof UnknownAccountException) {
errorMsg = "用户不存在";
} else if (e instanceof LockedAccountException) {
errorMsg = "用户被禁用";
} else if (e instanceof IncorrectCredentialsException) {
errorMsg = "密码错误";
} else {
errorMsg = "认证失败";
}
request.setAttribute("errorMsg", errorMsg);
return "/login";
}
return "redirect:/success";
}
// 登录成功页面
@GetMapping("/success")
public String success() {
return "success";
}
// 退出
@GetMapping("/logout")
public String logout() {
log.info("进入logout()方法");
SecurityUtils.getSubject().logout();
return "redirect:/login";
}
// 授权失败时,跳转到role页面
@GetMapping("/role")
public String role() {
return "role";
}
}
login.ftl:
登录页面
用户登录
${errorMsg}
success.ftl:
登录成功:${user.username}
role.ftl:
权限不足
5)、测试
针对之前配置类的拦截器中的配置进行测试:
filterMap.put("/guest/**", "anon");
访问:http://localhost:8080/guest/enter 的url,由于不需要认证,所以可以直接访问。
filterMap.put("/user/**", "roles[user]");
上述的url还没有按回车键,按后:
跳转到了这个登录界面(按回车键,跳转到登录界面,这个过程,我无法证明,但读者可以去试试。)。话说回来,为什么会跳转到登录界面?因为访问以"/user"开头的url是需要"user"的角色的,都还没有登录,怎么可能会让你访问呢?所以,需要你去登录。
执行登录逻辑:
进行登录操作时,会调用LoginController.doLogin()方法,会执行
SecurityUtils.getSubject().login(token);
上面那条语句,所以,会调用UserRealm.doGetAuthenticationInfo()方法进行认证(如果忘记了,请往上看UserRealm类那块)。
我们以“zzc-666”的用户进行登录,它的角色是"user"。
先使用错的账号进行登录:
再来使用一个错的账号:
使用对的账号和密码:
在登录成功界面,我们来访问“进入管理员页面的”链接,它的html代码为:
进入管理员页面
根据配置类中的配置:
filterMap.put("/admin/**", "roles[admin]");
可知,访问此条链接是需要管理员角色的,那么我这个"user"角色能否访问?
由此可知,权限不足,无法访问,那么为何会跳转到这个页面呢?
根据此配置:
shiroFilterFactoryBean.setUnauthorizedUrl("/role");
好了,SpringBoot集成Shiro的简单介绍就到这儿了。
【参考资料】
教你 Shiro 整合 SpringBoot,避开各种坑