一、权限管理
1、权限管理介绍
每个系统的权限功能都不尽相同,各有其自身的业务特点,对权限管理的设计也都各有特色。不过不管是怎样的权限设计,大致可归为三种:页面权限(菜单级)、操作权限(按钮级)、数据权限。当前系统只是讲解:菜单权限与按钮权限的控制。
1.1、菜单权限
菜单权限就是对页面的控制,就是有这个权限的用户才能访问这个页面,没这个权限的用户就无法访问,它是以整个页面为维度,对权限的控制并没有那么细,所以是一种粗颗粒权限。
1.2、按钮权限
按钮权限就是将页面的操作视为资源,比如删除操作,有些人可以操作有些人不能操作。对于后端来说,操作就是一个接口。于前端来说,操作往往是一个按钮,是一种细颗粒权限。
1.3、权限管理设计思路
前面我们讲解了用户管理、角色管理及菜单管理,我们把菜单权限分配给角色,把角色分配给用户,那么用户就拥有了角色的所有权限(权限包含:菜单权限与按钮权限)。
接下来需要实现这两个接口:
1、用户登录
2、登录成功根据token获取用户相关信息(菜单权限及按钮权限数据等)
用户登录我们需要用到JWT,接下来讲解JWT。
2、JWT
2.1、JWT介绍
JWT是JSON Web Token的缩写,即JSON Web令牌,是一种自包含令牌。 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
JWT最重要的作用就是对 token信息的防伪作用。
2.2、JWT令牌的组成
一个JWT由三个部分组成:JWT头、有效载荷、签名哈希
最后由这三者组合进行base64url编码得到JWT
典型的,一个JWT看起来如下图:该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
https://jwt.io/
JWT头
JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);
typ属性表示令牌的类型,JWT令牌统一写为JWT。
最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
{
"name": "Helen",
"role": "editor",
"avatar": "helen.jpg"
}
请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
JSON对象也使用Base64 URL算法转换为字符串保存。
签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret) ==> 签名hash
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。
Base64URL算法
如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。
作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+“,”/“和”=“,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。
2.3、项目集成JWT
操作模块:common-util
2.3.1、 引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
2.3.2、 添加JWT帮助类
package com.atguigu.common.jwt;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.util.Date;
public class JwtHelper {
//token有效时间
private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
//加密的秘钥
private static String tokenSignKey = "123456";
//根据用户id和用户名称生成token字符串
public static String createToken(Long userId, String username) {
String token = Jwts.builder()
.setSubject("AUTH-USER")//表示做个分类
//设置有效时间
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
//设置主体部分
.claim("userId", userId)
.claim("username", username)
//签名部分
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)//签名压缩
.compact();
return token;
}
//从生成的token中获取用户id
public static Long getUserId(String token) {
try {
if (StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer) claims.get("userId");
return userId.longValue();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
//从生成的token中获取用户名
public static String getUsername(String token) {
try {
if (StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("username");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
String token = JwtHelper.createToken(1L, "admin");
System.out.println(token);
System.out.println(JwtHelper.getUserId(token));
System.out.println(JwtHelper.getUsername(token));
}
}
3、用户登录
3.1、修改登录方法
修改IndexController类登录方法
@Api(tags = "后台登录管理")
@RestController
@RequestMapping("/admin/system/index")
public class IndexController {
@Autowired
private SysUserService sysUserService;
//login 登录
@PostMapping("login")
public Result login(@RequestBody LoginVo loginVo) {
//下面一大串代码 应该写在service中,这里写在了controller中了
//1 获取输入用户名和密码
//2 根据用户名查询数据库
String username = loginVo.getUsername();
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername,username);
SysUser sysUser = sysUserService.getOne(wrapper);
//3 用户信息是否存在
if(sysUser == null) {
throw new GuiguException(201,"用户不存在");
}
//4 判断密码
//数据库存密码(MD5)
String password_db = sysUser.getPassword();
//获取输入的密码 也要进行MD5加密 然后和数据库中的比 因为MD5不可逆
String password_input = MD5.encrypt(loginVo.getPassword());
if(!password_db.equals(password_input)) {
throw new GuiguException(201,"密码错误");
}
//5 判断用户是否被禁用 1 可用 0 禁用
if(sysUser.getStatus().intValue()==0) {
throw new GuiguException(201,"用户已经被禁用");
}
//6 使用jwt根据用户id和用户名称生成token字符串
String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());
//7 返回
Map<String,Object> map = new HashMap<>();
map.put("token",token);
return Result.ok(map);
}
4、获取用户信息
4.1、获取用户菜单权限
说明:获取菜单权限数据,我们要将菜单数据构建成路由数据结构
controller:
//info
@GetMapping("info")
public Result info(HttpServletRequest request) {
//1 从请求头获取用户信息(获取请求头token字符串)
String token = request.getHeader("token");
//2 从token字符串获取用户id 或者 用户名称
Long userId = JwtHelper.getUserId(token);
//3 根据用户id查询数据库,把用户信息获取出来
SysUser sysUser = sysUserService.getById(userId);
//4 根据用户id获取用户可以操作哪些菜单列表
//查询数据库动态构建路由结构,进行显示
List<RouterVo> routerList = sysMenuService.findUserMenuListByUserId(userId);
//5 根据用户id获取用户可以操作哪些按钮列表
List<String> permsList = sysMenuService.findUserPermsByUserId(userId);
//6 返回相应的数据
Map<String, Object> map = new HashMap<>();
map.put("roles","[admin]");
map.put("name",sysUser.getName());
map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
//返回用户可以操作的菜单
map.put("routers",routerList);
//返回用户可以操作的按钮
map.put("buttons",permsList);
return Result.ok(map);
}
service:
接口:
public interface SysMenuService extends IService<SysMenu> {
//4 根据用户id获取用户可以操作菜单列表
List<RouterVo> findUserMenuListByUserId(Long userId);
//5 根据用户id获取用户可以操作按钮列表
List<String> findUserPermsByUserId(Long userId);
}
实现类
//4 根据用户id获取用户可以操作菜单列表
@Override
public List<RouterVo> findUserMenuListByUserId(Long userId) {
List<SysMenu> sysMenuList = null;
//1 判断当前用户是否是管理员 userId=1是管理员
//1.1 如果是管理员,查询所有菜单列表
if(userId.longValue() == 1) {
//查询所有菜单列表
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getStatus,1);
wrapper.orderByAsc(SysMenu::getSortValue);
sysMenuList = baseMapper.selectList(wrapper);
} else {
//1.2 如果不是管理员,根据userId查询可以操作菜单列表
//多表关联查询:用户角色关系表 、 角色菜单关系表、 菜单表
sysMenuList = baseMapper.findMenuListByUserId(userId);
}
//2 把查询出来数据列表-构建成框架要求的路由结构
//使用菜单操作工具类构建树形结构
List<SysMenu> sysMenuTreeList = MenuHelper.buildTree(sysMenuList);
//构建成框架要求的路由结构
List<RouterVo> routerList = this.buildRouter(sysMenuTreeList);
return routerList;
}
联表查询用到了SQL, MP联表不如SQL方便
mapper:
接口:
@Mapper
public interface SysMenuMapper extends BaseMapper<SysMenu> {
//多表关联查询:用户角色关系表 、 角色菜单关系表、 菜单表
List<SysMenu> findMenuListByUserId(@Param("userId") Long userId);
}
SysMenuMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.auth.mapper.SysMenuMapper">
//自定义返回类型 实体类SysMenu
<resultMap id="sysMenuMap"
type="com.atguigu.model.system.SysMenu" autoMapping="true">
</resultMap>
//多表关联查询:用户角色关系表 、 角色菜单关系表、 菜单表
( 先得到的用户id(参数),所以先从用户角色关系表中得到角色,再通过角色从角色菜单表得到菜单)
<select id="findMenuListByUserId" resultMap="sysMenuMap">
select distinct <!-- 因为一个用户可能对应多个角色所以要对菜单出重>
m.id,m.parent_id,m.name,m.type,m.path,m.component,m.perms,m.icon,
m.sort_value,m.status,m.create_time,m.update_time,m.is_deleted
from sys_menu m
inner join sys_role_menu rm on rm.menu_id = m.id
inner join sys_user_role ur on ur.role_id = rm.role_id
where ur.user_id=#{userId}
and m.status = 1
and rm.is_deleted = 0
and ur.is_deleted = 0
and m.is_deleted = 0
</select>
</mapper>
测试界面
超级管理员界面:
给李四这个用户分配两个权限:用户管理、角色管理(没给添加按钮)。
李四用户界面:
添加是点不动的,因为管理员没给李四添加(按钮)的功能。
总结
当前我们已经实现前端菜单及按钮的权限控制,服务器端还没加任何控制,那么服务器端怎么控制呢?
其实很简单,就是要在页面按钮对应的controller方法上面加对应的权限控制,即在进入controller方法前判断当前用户是否有访问权限。
怎么实现呢?如果我们自己实现,那么肯定想到的就是Fillter加Aop就可以实现,有现成的开源技术框架吗?
答案是肯定的,如:Spring Security、Shiro等一系列开源框架可供选择。