springboot2.0框架下使用Shiro
介绍
Shiro是Apache旗下的开源项目,是一个简单易用的安全框架,提供包括认证、授权、加密、会话管理等诸多功能。Shiro使用了比较简单易懂易于使用的授权方式。Shiro属于轻量级框架,配置简单,应用广泛。在很多优秀的开源项目之中都有使用。
搭建springboot,网页部分
创建项目
搭建springboot开发环境
打开idea,点击文件(file)-->新建(new)-->新建文件(Project)
之后就会进入idea的新建项目之中,这里点击这个树叶加一个开关按钮的这个标志,之后点击下一步就可以了
之后这个就是,真正的配置界面,填好信息,就可以点击下一步,这里面比较重要的是这个javaVersion这里,默认是11版本。需要用户自己根据实际情况选择版本
在依赖选择这里,只选Web里面的spring web选项就可以了。然后点击下一步。
这里一般只看上面就可以了,项目名和项目位置最后面的名字可以不一致,不影响程序的运行
点击完成,再等maven项目下载所需的依赖就可以了。依赖下载完成,项目结构如下。(target在项目运行时,会生成)
添加依赖项
打开pom.xml文件在
org.apache.shiro.shiro-all是本文中需要使用的的依赖
com.alibaba.fastjson是一个json格式文件处理工具,可能会用到
org.apache.shiro
shiro-all
1.6.0
com.alibaba
fastjson
1.2.74
打开idea右侧的maven的,点击如图所示刷新按钮,下载依赖包
搭建网页前端内容
用几个前端网页,对接后端数据,方便调试。
第一个是首页面(需要登录才能进入)
第二个是用户的登录页面
第三个是提示用户没有授权的页面,他会自动跳转到首页面或者登录页面。
首先是第一个页面,简单的使用html网页写一个大大的首页,并对其简单的美化一下,非常清楚
首页
首页主体
然后是第二个页面,登录页面,使用form表单,action内容千万不要使用域名(ip)+端口之后是路径这种方式,则会导致每次提交表单,都会导致session不一致,直接写路径就好。还是死一样,简单的美化一下就好
系统登录
之后就是用户没有权限时跳转的页面,这里设置为打开页面5秒后,自动跳转到首页面或者登录页面。
你没有该权限访问
java部分
介绍
考虑到sql,mybatis,配置过于繁杂,所以项目中使用静态的数据代替sql。再模拟写一个service层的部分。
启动项
在项目的启动项Application类中设置一个新注解,这里使用@ComponentScan注解来扫描注解类,为了省事,直接使用**号扫描
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {
//扫描包,加载注解
"com.eelinker.shiro.**",
"com.eelinker.shiro.**.**",
"com.eelinker.shiro.**.**.**"
})
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
}
Service层
User
首先需要创建一个实体类,User,用户的数据信息封装,get和set以及toString方法省略。这里面有一个Role的类是没有定义的,代表的是用户的访问权限。
public class User {
//用户id
private int id;
//用户名
private String username;
//用户密码
private String password;
//用户权限
private Role role;
}
Role
这里设定了老板权限,员工权限,检查人员权限,以及顾客权限三大类型,详细的权限还分为管理权(administration),制作权(make),检查权(check)
顾客只能访问最基本的访问权限,其他的人都默认拥有顾客的访问权限,这些访问权限都需要登录,不登录都会默认跳转到登录界面。为了区分这些权限,特地的做了一个枚举类(不用枚举也行,用字符串数据代替也是可以的)
package com.eelinker.shiro.entity;
import java.util.HashSet;
import java.util.Set;
/**
*角色类型
*
* @author Administrator
*/
public enum Role {
//老板 所有权限
boss(permissionType.administration, permissionType.make, permissionType.check),
//员工 制作权限
staff(permissionType.make),
//检查人员 查询权限
checker(permissionType.check),
//顾客 使用权限
customer;
private final permissionType[] types;
Role(permissionType... types) {
this.types = types;
}
/**
* 权限类型
*/
public enum permissionType {
administration,
make,
check;
@Override
public String toString() {
return name();
}
}
/**
* 获取用户的权限名称
*/
public Set toSetValue() {
Set permsSet = new HashSet<>();
for (permissionType type : types) {
System.out.println(type.name());
permsSet.add(type.name());
}
return permsSet;
}
}
UserService
在将用户的信息封装好之后,接下来就是写Service的代码了,因为这里只是演示,所以只需要展示用户的访问权限就行了。静态的List集合代替数据库中的内容,select代替mybatis的查询语句封装。根据用户名查询用户的具体数据。
import com.eelinker.shiro.entity.Role;
import com.eelinker.shiro.entity.User;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* Created System: Windows7
* Created Code by Administrator
* Created Date: 2022/1/13 16:05
*
* @author Administrator
*/
@Service
public class UserService {
private static List userList = new ArrayList() {
{
add(new User(1, "admin", "admin123", Role.boss));
add(new User(2, "checker", "checker123", Role.checker));
add(new User(3, "staff_zhang", "staff003", Role.staff));
add(new User(4, "staff_li", "staff004", Role.staff));
add(new User(5, "staff_wang", "staff005", Role.staff));
add(new User(6, "staff_zhao", "staff006", Role.staff));
add(new User(7, "customer7", "customer", Role.customer));
add(new User(8, "customer8", "customer", Role.customer));
add(new User(9, "customer9", "customer", Role.customer));
add(new User(10, "customer10", "customer", Role.customer));
add(new User(11, "customer11", "customer", Role.customer));
}
};
private static User select(String username) {
for (User user : userList) {
if (user.getUsername().equals(username)) {
return user;
}
}
return null;
}
}
shiro配置部分
读取yml配置文件数据内容
在springboot中是可以自己设置yml中的键值对的。这样方便开发者修改,调试。这里是我自定义的shiro配置。使用shiro作为前缀。在shiro的下一级子参数中,login-url这个key值写成loginUrl也是可以的。
配置文件的名称定位application-shiro.yml(后缀改为yaml也是可以的)
shiro:
#登录地址url
login-url: /login.html
#未经授权的跳转
unauthorized-url: /unauthor.html
#登录成功的url
success-url: /index.html
filter-class:
- anon,org.apache.shiro.web.filter.authc.AnonymousFilter
- logout,org.apache.shiro.web.filter.authc.LogoutFilter
#路径过滤规则
filter-rule:
- /**.html,anon
- /index/**,anon
- /user/**,anon
#注销登录的地址,不需要开发者写相关的代码或业务,也能退出登录
- /logout,logout
#需要有这个名称的权限才能访问这个路径(这里没有提登录,不登录也会触发,之后跳转到unauthorized-url)
- /select/**,perms["check"]
- /**,authc
在写好了配置之后,这还是不能让开发者直接读取。需要在主配置相中配置,也就是application.yml这个文件。
下面这里server.port这里是修改项目端口的位置
spring.profiles.active这里是配置springboot,读取上面配置的application-shiro.yml文件。
server:
port: 8052
spring:
profiles:
active: shiro
messages:
encoding: UTF-8
main:
allow-bean-definition-overriding: true
在配置好上面信息之后,就可以创建一个实体类封装这些数据了。代码如下。因为配置中需要的是一组键值对而不是list集合,所以可以用“,”隔开两个两个数据前面的是路径和面的是具体的内容
filterClass是拦截器的类加载,在getFilterClassValue方法中会创建出这一个类,然后返回。
filterRule这是拦截路径,前面的是路径,后面的是允许访问的条件
@Component
@ConfigurationProperties(prefix = "shiro")
public class ShiroConfigReader {
private String loginUrl;
private String unauthorizedUrl;
private String successUrl;
private List filterClass;
private List filterRule;
// get set toString省略,在idea中可以使用alt+insert键创建
public Map getFilterClassValue() throws Exception {
Map map = new HashMap<>();
if (filterClass != null) {
for (String string : filterClass) {
String[] keyAndValue = string.split(",");
if (keyAndValue.length == 2) {
Class aClass = ClassLoader.getSystemClassLoader().loadClass(keyAndValue[1]);
Object object = aClass.newInstance();
if (object instanceof Filter) {
map.put(keyAndValue[0], (Filter) object);
}
}
}
if (map.size() != 0) {
return map;
}
}
throw new IllegalAccessException("The parameter is empty or the value is incorrect");
}
public Map getFilterRuleMap() {
Map map = new HashMap<>();
if (filterRule != null) {
for (String string : filterRule) {
String[] keyAndValue = new String[2];
int index = string.indexOf(",");
if (index != -1) {
keyAndValue[0] = string.substring(0, index);
keyAndValue[1] = string.substring(index + 1);
map.put(keyAndValue[0], keyAndValue[1]);
} else {
throw new IllegalArgumentException("The yml file parameter is incorrect");
}
}
return map;
}
throw new IllegalArgumentException("The yml file parameter is incorrect");
}
}
拿到这些配置信息之后,就可以根据内容读取数据信息,之后的修改和添加都可以在yml中进行。之前的上一个java文件中有@Component这个注解,所以在下面的shiro中可以使用@Autowired读取。在shiroFilterFactoryBean读取加载这些信息
import com.eelinker.shiro.config.reader.ShiroConfigReader;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* springBoot整合jwt实现认证有三个不一样的地方,对应下面abc
*
* @author Administrator
*/
@Configuration
public class ShiroConfig {
@Autowired
private ShiroConfigReader reader;
@Bean
public Realm realm() {
return new MyRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
// 关闭 ShiroDAO 功能
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 不需要将 Shiro Session 中的东西存到任何地方(包括 Http Session 中)
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//禁止Subject的getSession方法
// securityManager.setSubjectFactory(subjectFactory());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() throws Exception {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl(reader.getLoginUrl());
shiroFilter.setUnauthorizedUrl(reader.getUnauthorizedUrl());
shiroFilter.setFilters(reader.getFilterClassValue());
shiroFilter.setFilterChainDefinitionMap(reader.getFilterRuleMap());
return shiroFilter;
}
}
在第一个的@Bean中里面创建了一个MyRealm这个类的对象。这里就是用户的授权认证所需要用到的内容。
doGetAuthorizationInfo方法,判断当前访问的用户是否具有访问权限,例如检查,管理制作权限。这里会查询用户的数据信息,使用到数据库验证,读取用户信息进行验证。
doGetAuthenticationInfo这个方法师在用户调用subject.login(token)时,会进入到这里进行权限的配置。
import com.eelinker.shiro.entity.User;
import com.eelinker.shiro.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService service;
/**
* 用户身份认证
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) principalCollection.getPrimaryPrincipal();
User user = service.selectByUsername(username);
// 权限Set集合
Set permsSet = user.getRole().toSetValue();
// 返回权限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}
/**
* 用户权限认证(登录)
* 这个token就是从过滤器中传入的jwtToken
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//这里返回的是类似账号密码的东西,但是jwtToken都是jwt字符串。还需要一个该Realm的类名
return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), "MyRealm");
}
}
但是查询用户所拥有的权限这里,没有userService.selectByUsername方法,所以到userService中添加一个这个方法
public User selectByUsername(String username) {
for (User user : userList) {
if (user.getUsername().equals(username)) {
return user;
}
}
return null;
}
Controller简单创建
在前面创建的页面中有一个登录页面,那么现在就进行登录操作。类的创建就省略了,这里自动注入了一个UserService。mapping中可以验证
@Autowired
private UserService userService;
/**
* 用户登录
*
* @param username
* @param password
* @return
*/
@RequestMapping("/user/login")
public String login(@RequestParam String username, @RequestParam String password) {
User user = userService.login(username, password);
if (user != null) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token);
return "redirect:/index.html";
}
return "redirect:/login.html";
}
但是目前没有写验证的service代码。回到UserService添加如下代码,验证用户名和密码是否正确。
public User login(String username, String password) {
User user = select(username);
if (user == null) {
throw new NullPointerException("User does not exist !");
}
if (user.getPassword().equals(password)) {
return user;
} else {
System.out.println("密码错误");
}
return null;
}
从下图中可以看到,先输入的纯域名,之后跳转到了login.html就表名shiro的拦截页面起了效果
好,看看登录页面,输入下面数据,查看是否登录成功,成功就会进入index.html页面
回到代码,写一个需要有权限才能访问的页面,这里面的select在之前的yml文件中是已经配置过的了。需要有查询权限才能访问。代码写好重启,去网页上访问
@ResponseBody
public Map getInfo() throws Exception {
Map map = new HashMap<>();
map.put("filterRule", reader.getFilterRuleMap());
map.put("filterClass", reader.getFilterClassValue());
map.put("loginUrl", reader.getLoginUrl());
map.put("unauthorizedUrl", reader.getUnauthorizedUrl());
map.put("successUrl", reader.getSuccessUrl());
return map;
}
网页输入localhost:8052/select/info,执行访问,由于已经重启,多以他会跳转到登录页面,这时候需要重新登录。(如果还是在首页面,那就是网页缓存的问题,清一下就好)
如果出现一下的状态,就是没问题的
如果用户没用登录,或者登录了但是没有这个查验的权利呢?用户呢?
输入localhost:8052/logout注销登录,再次输入localhost:8052/select/info尝试。
没登录的用户不能访问,会跳转到登录页面,而登录的用户,却没有这个权限的,会跳转到如下页面,之后再跳转到登录页面