一文掌握springboot集成shiro安全框架
xsobi 2024-12-15 17:31 1 浏览
前言:
Apache Shiro是一个强大而灵活的开源安全框架,具有易于集成、可扩展性强和安全性高等特点,适用于各种Java应用程序的安全控制场景。它提供了认证、授权、加密、会话管理以及与Web集成等安全功能,帮助开发者轻松地实现应用程序的安全控制。
一、主要功能
- 认证(Authentication):对用户进行身份验证。
- 授权(Authorization):验证某个已认证的用户是否拥有访问某个资源或执行某个操作的权限。这通常涉及到角色和权限的管理。
- 加密(Cryptography):Shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加密/解密等功能,以保护敏感数据的安全性。
- 会话管理(Session Management):Shiro定义了一套会话管理机制,它不依赖于Web容器的session,因此可以在非Web应用上使用。Shiro还可以将分布式应用的会话集中在一点管理,实现单点登录。
- 与Web集成(Web Integration):Shiro可以轻松地与Web应用程序集成,为Web应用提供安全控制。
二、核心组件
- Subject:即“当前操作用户”,但不仅限于人,也可以是第三方进程、后台帐户等。它代表了当前用户的安全操作。
- SecurityManager:Shiro框架的核心,负责认证、授权等业务实现。Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
- Realm:充当了Shiro与应用安全数据间的“桥梁”或“连接器”。当对用户执行认证和授权验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。(简单理解就是dao层或者mapper层,与数据库交互)
三、工作流程
1.认证流程
- 用户通过登录表单或其他认证接口提交其身份标识(如用户名、邮箱等)和凭证(如密码、密钥等。
- Shiro使用AuthenticationToken接口来封装用户的身份标识和凭证信息,将创建好的AuthenticationToken实例提交Shiro进行认证。
- SecurityManager接收认证请求并委托给Authenticator进行处理。
- Authenticator调用Realm获取用户信息、角色和权限。
- Realm执行认证逻辑,在doGetAuthenticationInfo方法中,根据认证令牌中的身份标识查询用户信息。如果用户存在,则返回一个包含用户信息、凭证(通常已经加密)和Realm名称的AuthenticationInfo实例。
- CredentialsMatcher进行凭证匹配,会比较用户提交的凭证和从Realm获取的加密凭证是否匹配
- 认证结果处理,如果凭证匹配成功,则认证通过,用户被标记为已认证状态。如果凭证匹配失败或用户不存在,则抛出AuthenticationException异常,认证失败
2.授权流程:
- 用户尝试访问受保护资源,Shiro会拦截该请求并进行授权检查。
- SecurityManager会委托给Authorizer进行授权检查。
- Authorizer调用Realm获取用户角色和权限信息。
- Realm执行授权逻辑,在doGetAuthorizationInfo方法中,根据用户的身份标识查询其角色和权限信息,返回一个包含角色和权限信息的AuthorizationInfo实例。
- 授权结果处理,Shiro根据用户的角色和权限信息来判断用户是否有权访问该资源。如果用户有权访问,则授权通过,用户可以访问该资源。如果用户无权访问,则抛出AuthorizationException异常或执行其他拒绝访问的逻辑。
四,代码演示:
下面案例使用根据用户名/密码生成token,自定义过滤器,让所有的请求走自定义过滤器。案例使用用户信息在内存进行演示。
1.基础依赖
<!--springboot版本-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.7.RELEASE</version>
<relativePath/>
</parent>
<!--相关依赖-->
<dependencies>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>${shiro.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.29</version>
</dependency>
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
</dependencies>
2.定义基本对象与工具
- 用户对象,统一返回结果对象
//用户对象
@Data
public class LoginUser {
//用户名
private String username;
//密码
private String password;
//角色码
private Set<String> roleSet;
//权限码
private Set<String> permissionSet;
}
//返回统一对象
@Data
public class Result<T> {
private String msg;
private Integer code;
private T data;
public Result() {
}
public Result(Integer code, String message) {
this.code = code;
this.msg = message;
}
public static <T> Result<T> ok(String msg, T data) {
return restResult(data, 200, msg);
}
public static <T> Result<T> fail(int code, String msg) {
return restResult(null, code, msg);
}
private static <T> Result<T> restResult(T data, int code, String msg) {
Result<T> r = new Result();
r.setCode(code);
r.setData(data);
r.setMsg(msg);
return r;
}
}
- jwt工具类
@Slf4j
public class JwtUtil {
//过期时间
public static final long EXPIRE_TIME = 1 * 60 * 1000;
//
public static void responseError(ServletResponse response, Integer code, String errorMsg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
Result result = new Result(code, errorMsg);
ServletOutputStream os = null;
try {
os = httpServletResponse.getOutputStream();
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setStatus(401);
os.write((new ObjectMapper()).writeValueAsString(result).getBytes("UTF-8"));
os.flush();
os.close();
} catch (IOException e) {
log.error(e.getMessage());
}
}
/**
* 校验token是否正确
*
* @param token 密钥
* @param password 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String password) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(password);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 生成token,1min后过期
*
* @param username 用户名
* @param password 用户的密码
* @return 加密的token
*/
public static String createToken(String username, String password) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(password);
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
}
}
- 全局异常捕获
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
//权限异常
@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
public Result<Void> handleBaseException(AuthorizationException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
return Result.fail(403, "没有访问权限,请联系管理员授权");
}
}
3. 实现AuthenticationToken
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtToken(String token) {
this.token = token;
}
public Object getPrincipal() {
return this.token;
}
public Object getCredentials() {
return this.token;
}
}
4.继承AuthorizingRealm(认证,授权)
@Component
public class ShiroRealm extends AuthorizingRealm {
private static final Logger log = LoggerFactory.getLogger(ShiroRealm.class);
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
//权限认证
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("===============权限认证==============");
String username = null;
if (principals != null) {
LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
username = sysUser.getUsername();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//这一步正常情况 !!!查数据库【本次演示使用内存】
LoginUser loginUser = CacheMap.CACHE_MAP.get(username);
// 设置用户拥有的角色集合,比如“admin,test”
info.setRoles(loginUser.getRoleSet());
// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
info.addStringPermissions(loginUser.getPermissionSet());
return info;
}
//身份认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
log.info("===============身份认证==============");
String token = (String) auth.getCredentials();
if (StringUtils.isEmpty(token)) {
throw new AuthenticationException("token为空!");
} else {
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token非法无效!");
}
//查询用户信息 !!!(这一步正常情况,查数据库【本次演示使用内存】)
LoginUser loginUser = CacheMap.CACHE_MAP.get(username);
if (loginUser == null) {
throw new AuthenticationException("用户不存在!");
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!JwtUtil.verify(token, username, loginUser.getPassword())) {
throw new AuthenticationException("token失效,请重新登录!");
}
return new SimpleAuthenticationInfo(loginUser, token, this.getName());
}
}
}
5.自定义过滤器
public class JwtFilter extends BasicHttpAuthenticationFilter {
//会检查当前Subject是否有权限访问请求的URI
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
this.executeLogin(request, response);
return true;
} catch (Exception e) {
JwtUtil.responseError(response, 401, e.getMessage());
return false;
}
}
return true;
}
//查询请求头是否有token
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
return token != null;
}
//执行登录
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
JwtToken jwtToken = new JwtToken(token);
this.getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
//对跨域提供支持
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 是否允许发送Cookie,默认Cookie不包括在CORS请求之中。设为true时,表示服务器允许Cookie包含在请求中。
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
方法的执行顺序: preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin
6.配置shiro
@Configuration
public class ShiroConfig {
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加自定义过滤器
Map<String, Filter> filterMap = new HashMap<>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 配置Shiro Filter 规则
Map<String, String> filterChainMap = new LinkedHashMap<>();
// 配置不会被拦截的链接 顺序判断
filterChainMap.put("/sys/login", "anon");
// 其他路径需要认证
filterChainMap.put("/**", "authc");
//请求经过自定义的过滤器
filterChainMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
五:测试
- 内存数据对象
@Component
public class CacheMap {
public static Map<String, LoginUser> CACHE_MAP = new HashMap();
static {
LoginUser loginUser = new LoginUser();
loginUser.setUsername("小明");
loginUser.setPassword("123456");
//角色编码
HashSet<String> roleSet = new HashSet<>();
roleSet.add("admin");
loginUser.setRoleSet(roleSet);
//权限编码
HashSet<String> permissionSet = new HashSet<>();
permissionSet.add("sys:user:add");
permissionSet.add("sys:user:query");
loginUser.setPermissionSet(permissionSet);
CACHE_MAP.put(loginUser.getUsername(), loginUser);
}
}
1.登录生成token
这里jwt生成token,我设置了有效期,有效期是1分钟。
示例代码(java代码)
@RequestMapping("/sys")
@RestController
public class UserController {
@PostMapping("/login")
public Result login(@RequestBody LoginUser loginUser) {
//这一步正常情况 !!!查数据库【本次演示使用内存】
LoginUser user = CacheMap.CACHE_MAP.get(loginUser.getUsername());
if (!Objects.equals(user.getPassword(), loginUser.getPassword())) {
new Result(-100, "密码错误");
}
//生成token
String token = JwtUtil.createToken(loginUser.getUsername(), loginUser.getPassword());
HashMap<String, Object> data = new HashMap<>();
data.put("user", user);
data.put("token", token);
return Result.ok("登录成功", data);
}
}
执行结果:
2.验证角色权限,资源权限
- @RequiresRoles:角色权限:注解用于指定访问用户必须拥有该角色的权限才能执行某个操作。
- @RequiresPermissions:资源权限:注解用于指定用户必须拥有哪些具体的权限才能执行某个操作。
a.测试角色权限:
示例代码(java代码)
这里表示:当前访问的用户,需要有dev角色权限,接口权限码sys:user:query才能访问。
@RequestMapping("/sys")
@RestController
public class UserController {
@GetMapping("/queryUserName")
@RequiresRoles({"dev"})
@RequiresPermissions("sys:user:query")
public Result getUsername(LoginUser loginUser) {
LoginUser user = CacheMap.CACHE_MAP.get(loginUser.getUsername());
return Result.ok("成功", user);
}
}
当前测试小明只有admin权限(正常情况admin有全部权限,这里演示不讨论),没有dev权限。
给小明加上dev角色权限
执行结果:
b.测试接口权限:
示例代码(java代码)
@RequestMapping("/sys")
@RestController
public class UserController {
@GetMapping("/add")
@RequiresRoles(value = "admin")
@RequiresPermissions("user:add")
public Result add(LoginUser loginUser) {
CacheMap.CACHE_MAP.put(loginUser.getUsername(), loginUser);
return Result.ok("新增用户成功", loginUser);
}
}
执行结果:
这里没有给小明接口权限码user:add
给小明加上接口权限码user:add
c.测试多个角色
1.示例代码(java代码)
@RequiresRoles({"admin","dev","test"}):这种写法默认用户需要拥有这个三个角色才能访问。
@RequestMapping("/sys")
@RestController
public class UserController {
//这里用户只要admin,dev角色,没有test角色
@GetMapping("/query")
@RequiresRoles({"admin","dev","test"})
@RequiresPermissions("query")
public Result query(LoginUser loginUser) {
LoginUser user = CacheMap.CACHE_MAP.get(loginUser.getUsername());
return Result.ok("成功", user);
}
}
执行结果:
2.示例代码(java代码)
@RequiresRoles(value = {"admin","dev","test"},logical = Logical.OR):这种写法用户需要只要拥有其中一个角色权限就能访问。
@RequestMapping("/sys")
@RestController
public class UserController {
@GetMapping("/query")
//Logical.OR:表示当前用户只要有其中一个角色权限,就可以访问
@RequiresRoles(value = {"admin","dev","test"},logical = Logical.OR)
public Result query(LoginUser loginUser) {
LoginUser user = CacheMap.CACHE_MAP.get(loginUser.getUsername());
return Result.ok("成功", user);
}
}
执行结果:
六:注解解释
- @RequiresAuthentication:标注在方法或类上,表示当前用户需要进行身份认证才能访问该方法或类。
- @RequiresUser:标注在方法或类上,表示当前用户必须是已经进行过身份认证或者通过“记住我”功能登录的用户才能访问该方法或类。
- @RequiresGuest:标注在方法或类上,表示当前用户必须是一个“guest”用户,即未进行身份认证或者未通过“记住我”功能登录的用户才能访问该方法或类。
- @RequiresRoles:标注在方法或类上,表示当前用户必须具有指定的角色才能访问该方法或类。例如,@RequiresRoles(value={"admin", "user"}, logical=Logical.AND)表示当前用户需要同时具有admin和user角色;@RequiresRoles(value={"admin", "user"}, logical=Logical.OR)表示当前用户需要具有admin或user角色之一。
- @RequiresPermissions:标注在方法或类上,表示当前用户必须具有指定的权限才能访问该方法或类。例如,@RequiresPermissions(value={"user:a", "user:b"}, logical=Logical.OR)表示当前用户需要具有user:a或user:b权限之一。
相关推荐
- js向对象中添加元素(对象,数组) js对象里面添加元素
-
一、添加一个元素对象名["属性名"]=值(值:可以是一个值,可以是一个对象,也可以是一个数组)这样添加进去的元素,就是一个值或对象或数组...
- JS小技巧,如何去重对象数组?(一)
-
大家好,关于数组对象去重的业务场景,想必大家都遇到过类似的需求吧,这对这样的需求你是怎么做的呢。下面我就先和大家分享下如果是基于对象的1个属性是怎么去重实现的。方法一:使用.filter()和....
- 「C/C++」之数组、vector对象和array对象的比较
-
数组学习过C语言的,对数组应该都不会陌生,于是这里就不再对数组进行展开介绍。模板类vector模板类vector类似于string,也是一种动态数组。能够在运行阶段设置vector对象的长度,可以在末...
- 如何用sessionStorage保存对象和数组
-
背景:在工作中,我将[{},{}]对象数组形式,存储到sessionStorage,然后ta变成了我看不懂的形式,然后我想取之用之,发现不可能了~记录这次深刻的教训。$clickCouponIndex...
- JavaScript Array 对象 javascript的array对象
-
Array对象Array对象用于在变量中存储多个值:varcars=["Saab","Volvo","BMW"];第一个数组元素的索引值为0,第二个索引值为1,以此类推。更多有...
- JavaScript中的数组Array(对象) js array数组
-
1:数组Array:-数组也是一个对象-数组也是用来存储数据的-和object不同,数组中可以存储一组有序的数据,-数组中存储的数据我们称其为元素(element)-数组中的每一个元素都有一...
- 数组和对象方法&数组去重 数组去重的5种方法前端
-
列举一下JavaScript数组和对象有哪些原生方法?数组:arr.concat(arr1,arr2,arrn);--合并两个或多个数组。此方法不会修改原有数组,而是返回一个新数组...
- C++ 类如何定义对象数组?初始化数组?linux C++第43讲
-
对象数组学过C语言的读者对数组的概念应该很熟悉了。数组的元素可以是int类型的变量,例如int...
- ElasticSearch第六篇:复合数据类型-数组,对象
-
在ElasticSearch中,使用JSON结构来存储数据,一个Key/Value对是JSON的一个字段,而Value可以是基础数据类型,也可以是数组,文档(也叫对象),或文档数组,因此,每个JSON...
- 第58条:区分数组对象和类数组对象
-
示例设想有两个不同类的API。第一个是位向量:有序的位集合varbits=newBitVector;bits.enable(4);bits.enable([1,3,8,17]);b...
- 八皇后问题解法(Common Lisp实现)
-
如何才能在一张国际象棋的棋盘上摆上八个皇后而不致使她们互相威胁呢?这个著名的问题可以方便地通过一种树搜索方法来解决。首先,我们需要写一个函数来判断棋盘上的两个皇后是否互相威协。在国际象棋中,皇后可以沿...
- visual lisp修改颜色的模板函数 怎么更改visual studio的配色
-
(defunBF-yansemokuai(tuyuanyanse/ss)...
- 用中望CAD加载LISP程序技巧 中望cad2015怎么加载燕秀
-
1、首先请加载lisp程序,加载方法如下:在菜单栏选择工具——加载应用程序——添加,选择lisp程序然后加载,然后选择添加到启动组。2、然后是添加自定义栏以及图标,方法如下(以...
- 图的深度优先搜索和广度优先搜索(Common Lisp实现)
-
为了便于描述,本文中的图指的是下图所示的无向图。搜索指:搜索从S到F的一条路径。若存在,则以表的形式返回路径;若不存在,则返回nil。...
- 两个有助于理解Common Lisp宏的例子
-
在Lisp中,函数和数据具有相同的形式。这是Lisp语言的一个重大特色。一个Lisp函数可以分析另一个Lisp函数;甚至可以和另一个Lisp函数组成一个整体,并加以利用。Lisp的宏,是实现上述特色的...
- 一周热门
- 最近发表
- 标签列表
-
- grid 设置 (58)
- 移位运算 (48)
- not specified (45)
- patch补丁 (31)
- strcat (25)
- 导航栏 (58)
- context xml (46)
- scroll (43)
- element style (30)
- dedecms模版 (53)
- vs打不开 (29)
- nmap (30)
- webgl开发 (24)
- parse (24)
- c 视频教程下载 (33)
- android 开发环境 (24)
- paddleocr (28)
- listview排序 (33)
- firebug 使用 (31)
- transactionmanager (30)
- characterencodingfilter (33)
- getmonth (34)
- commandtimeout (30)
- hibernate教程 (31)
- label换行 (33)