参考:
https://blog.csdn.net/qq_43948583/article/details/104437752
https://blog.csdn.net/weixin_42375707/article/details/111145907
计划实现增删改查级别的权限控制
1. 表结构
菜单表
CREATE TABLE `menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
`parent_id` bigint(20) NOT NULL COMMENT '父节点id',
`url` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '路径',
`sort_id` int(11) DEFAULT '0' COMMENT '排序ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表';
菜单-操作表
CREATE TABLE `menu_opt` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
`opt_id` bigint(20) NOT NULL COMMENT '操作ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单-操作关联表';
操作表
CREATE TABLE `opt` (
`opt_id` bigint(20) NOT NULL AUTO_INCREMENT,
`opt_name` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作名称',
PRIMARY KEY (`opt_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作表';
角色表
CREATE TABLE `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
角色-菜单-操作表
CREATE TABLE `role_menu_opt` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
`opt_id` bigint(20) NOT NULL COMMENT '操作ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单-操作关联表表';
用户表
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) NOT NULL,
`pwd` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
用户角色表
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
2. 使用mybatis plus自动生成一些类
可以参考:https://www.jianshu.com/p/c984ac0b67e8
3. 添加shiro、jwt依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.3</version>
</dependency>
4. shiro、jwt配置
- shiro config
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//关联 DefaultWebSecurityManager
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro内置的过滤器
/*
* anon: 无需认证就可以访问
* auth: 必须认证才能访问
* user: 必须拥有记住我功能才能用
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
* */
// 添加过滤器
Map<String, Filter> filters = new HashMap<>(4);
// 自定义的认证授权过滤器
filters.put("auth", new AuthFilter());
// 添加自定义的认证授权过滤器
shiroFilterFactoryBean.setFilters(filters);
//要拦截的路径放在map里面
Map<String, String> filterMap = new LinkedHashMap<>();
// 放行login接口
filterMap.put("/login", "anon");
// 放行logout接口
filterMap.put("/logout", "anon");
// 拦截所有路径, 它自动会跑到 AuthFilter这个自定义的过滤器里面
filterMap.put("/**", "auth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(AuthRealm authRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//关联realm
defaultWebSecurityManager.setRealm(authRealm);
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
return defaultWebSecurityManager;
}
// 将自定义realm注入到 DefaultWebSecurityManager
@Bean
public AuthRealm authRealm() {
return new AuthRealm();
}
// 通过调用Initializable.init()和Destroyable.destroy()方法,从而去管理shiro bean生命周期
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
// 开启shiro权限注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(defaultWebSecurityManager);
return advisor;
}
}
- realm
public class AuthRealm extends AuthorizingRealm {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RoleMenuOptMapper roleMenuOptMapper;
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取前端传来的token
String accessToken = (String) token.getPrincipal();
// 根据token去缓存里查找用户名
String userId = JwtUtils.getAudience(accessToken);
if (userId == null) {
// 查找的用户名为空,即为token失效
throw new IncorrectCredentialsException("token失效,请重新登录");
}
JwtUtils.verifyToken(accessToken, userId);
SysUser user = sysUserMapper.selectById(Long.valueOf(userId));
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
// 此方法需要返回一个AuthenticationInfo类型的数据
// 因此返回一个它的实现类SimpleAuthenticationInfo,将user以及获取到的token传入它可以实现自动认证
return new SimpleAuthenticationInfo(user, accessToken, "");
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//从认证那里获取到用户对象User
SysUser user = (SysUser) principals.getPrimaryPrincipal();
// 此方法需要一个AuthorizationInfo类型的返回值,因此返回一个它的实现类SimpleAuthorizationInfo
// 通过SimpleAuthorizationInfo里的addStringPermission()设置用户的权限
QueryWrapper<UserRole> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", user.getId());
List<UserRole> userRoles = userRoleMapper.selectList(queryWrapper);
List<Long> roleIds = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toList());
QueryWrapper<RoleMenuOpt> optWrapper = new QueryWrapper<>();
optWrapper.in("role_id", roleIds);
List<RoleMenuOpt> roleMenuOpts = roleMenuOptMapper.selectList(optWrapper);
List<String> optList = roleMenuOpts.stream().map(opt -> String.valueOf(opt.getOptId()))
.collect(Collectors.toList());
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(optList);
return simpleAuthorizationInfo;
}
// @Override
// public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
// //自定义密码匹配器
// MyCredentialsMatcher currentCredentialsMatcher = new MyCredentialsMatcher();
// super.setCredentialsMatcher(currentCredentialsMatcher);
// }
}
- shiro filter
public class AuthFilter extends AuthenticatingFilter {
// 生成自定义token
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//从header中获取token
String token = httpServletRequest.getHeader("token");
return new JwtToken(token);
}
// 所有请求全部拒绝访问
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 允许option请求通过
return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
}
// 拒绝访问的请求,onAccessDenied方法先获取 token,再调用executeLogin方法
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 获取请求token
String token = httpServletRequest.getHeader("token");
// StringUtils.isBlank(String str) 判断str字符串是否为空或者长度是否为0
if (token == null || "".equals(token)) {
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setCharacterEncoding("UTF-8");
Response msg = Response.fail("请先登录");
httpServletResponse.getWriter().write(JSON.toJSONString(msg));
return false;
}
return executeLogin(request, response);
}
// token失效时调用
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpResponse.setCharacterEncoding("UTF-8");
try {
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
Response msg = Response.fail("登录凭证已失效,请重新登录");
httpResponse.getWriter().write(JSON.toJSONString(msg));
} catch (IOException e1) {
}
return false;
}
}
- jwt util
public class JwtUtils {
/**
* 签发对象:这个用户的id
* 签发时间:现在
* 有效时间:30分钟
* 载荷内容:暂时设计为:这个人的名字,这个人的昵称
* 加密密钥:这个人的id加上一串字符串
*/
public static String createToken(String userId, String realName, String userName) {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE, 30);
Date expiresDate = nowTime.getTime();
return JWT.create()
// 签发对象
.withAudience(userId)
// 发行时间
.withIssuedAt(new Date())
// 有效时间
.withExpiresAt(expiresDate)
// 载荷,随便写几个都可以
.withClaim("userName", userName)
.withClaim("realName", realName)
// 加密
.sign(Algorithm.HMAC256(userId + "HelloLehr"));
}
/**
* 检验合法性,其中secret参数就应该传入的是用户的id
*
* @param token
*/
public static void verifyToken(String token, String secret) {
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret + "HelloLehr")).build();
jwt = verifier.verify(token);
} catch (Exception e) {
// 效验失败
// 这里抛出的异常是我自定义的一个异常,你也可以写成别的
throw new TokenUnavailableException();
}
}
/**
* 获取签发对象
*/
public static String getAudience(String token) {
String audience = null;
try {
audience = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
// 这里是token解析失败
throw new TokenUnavailableException();
}
return audience;
}
/**
* 通过载荷名字获取载荷的值
*/
public static Claim getClaimByName(String token, String name) {
return JWT.decode(token).getClaim(name);
}
}
jwt token
public class JwtToken extends UsernamePasswordToken {
String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
有了以上类,基本上计算实现了,接下来写登录
5. 登录
login controller
@RestController
public class LoginController {
@Autowired
private SysUserMapper sysUserMapper;
@PostMapping("/login")
public Response login(@RequestParam("username") String username, @RequestParam("password") String password) {
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
if (sysUser == null) {
return Response.fail("账号或密码错误");
}
if (!sysUser.getPwd().equals(password)) {
return Response.fail("账号或密码错误");
}
String token = JwtUtils.createToken(sysUser.getId().toString(), sysUser.getUsername(), sysUser.getUsername());
return Response.success("登录成功").add("token", token);
}
}
6. 测试controller
@RestController
@RequestMapping("/menu")
@Slf4j
public class MenuController {
@Autowired
private MenuService menuService;
@GetMapping("/view")
@RequiresPermissions("1")
public Response view() {
return Response.success("success").add("menu", menuService.list());
}
@GetMapping("/add")
@RequiresPermissions("2")
public Response add() {
log.info("menu add.....");
return Response.success("success");
}
}
网友评论