一.验证码功能实现
(一)使用google kaptcha生成验证码
1.添加验证码配置类
新建yeb/yeb-server/src/main/java/com/cxy/server/config/CaptchaConfig.java
package com.cxy.server.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* @author 陈鑫元
* @description 验证码配置类
* @date 2021-05-24 18:00
* @since 1.0.0
*/
@Configuration
public class CaptchaConfig {
@Bean
public DefaultKaptcha defaultKaptcha() {
// 验证码生成器
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
// 配置
Properties properties = new Properties();
// 是否有边框
properties.setProperty("kaptcha.border", "yes");
// 设置边框颜色
//properties.setProperty("kaptcha.border.color","105,179,90");
properties.setProperty("kaptcha.border.color", "224,224,224");
// 边框粗细度,默认为1
properties.setProperty("kaptcha.border.thickness", "1");
// 设置验证码文本字符颜色,默认黑色
//properties.setProperty("kaptcha.textproducer.font.color", "blue");
// 设置字体样式
properties.setProperty("kaptcha.textproducer.font.names", "微软雅黑");
//properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
// 验证码 session key
properties.setProperty("kaptcha.session.key", "code");
// 验证码图片宽度,默认为 200
properties.setProperty("kaptcha.image.width", "100");
// 验证码图片高度,默认为40
properties.setProperty("kaptcha.image.height", "40");
// 字体大小
properties.setProperty("kaptcha.textproducer.font.size", "30");
// 验证码长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
// 没有干扰
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
2.写验证码接口
新建yeb/yeb-server/src/main/java/com/cxy/server/controller/CaptchaController.java
package com.cxy.server.controller;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
/**
* @author 陈鑫元
* @description 验证码接口
* @date 2021-05-24 18:03
* @since 1.0.0
*/
@RestController
public class CaptchaController {
@Autowired
private DefaultKaptcha defaultKaptcha;
@ApiOperation(value = "验证码")
@GetMapping(value = "/captcha", produces = "image/jpeg")
public void captcha(HttpServletRequest request, HttpServletResponse response) {
// 定义 response 输出类型为 image/jpeg 类型
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store,no-cache,must-revalidate");
response.addHeader("Cache-Control", "post-check=0,pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
// 生成验证码 开始
String text = defaultKaptcha.createText(); // 获取验证码文本内容
System.out.println("验证码文本内容:" + text);
request.getSession().setAttribute("captcah", text);
BufferedImage image = defaultKaptcha.createImage(text); // 根据文本内容创建图形验证码
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
ImageIO.write(image, "jpg", outputStream); // 输出流输出图片,格式为jpg
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 生成验证码 结束
// 查看验证码: http://localhost:8081/captcha
}
}
(二)校验验证码
1.登录实体类添加验证码字段
修改yeb/yeb-server/src/main/java/com/cxy/server/pojo/AdminLoginParam.java
2.登录Service添加验证码字段
修改yeb/yeb-server/src/main/java/com/cxy/server/service/IAdminService.java
3.登录Service实现类实现校验验证码功能
修改yeb/yeb-server/src/main/java/com/cxy/server/service/impl/AdminServiceImpl.java
package com.cxy.server.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cxy.server.config.security.component.JwtTokenUtil;
import com.cxy.server.mapper.AdminMapper;
import com.cxy.server.pojo.Admin;
import com.cxy.server.service.IAdminService;
import com.cxy.server.utils.RespBean;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
*
* 服务实现类
*
*
* @author 陈鑫元
* @since 2021-05-21
*/
@Service
public class AdminServiceImpl extends ServiceImpl implements IAdminService {
@Autowired
private UserDetailsService userDetailsService; // 权限框架的
@Autowired
private PasswordEncoder passwordEncoder; // 安全框架-密码加密解密
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHead}")
private String tokenHead; // token 头部信息
@Autowired
private AdminMapper adminMapper;
/**
* 登录之后返回 token
*
* @param username
* @param password
* @param code
* @param request
* @return
*/
@Override
public RespBean login(String username, String password, String code, HttpServletRequest request) {
// 校验验证码
String captcha = (String) request.getSession().getAttribute("captcah");
if (StringUtils.isEmpty(code) || !captcha.equalsIgnoreCase(code)) {
return RespBean.error("验证码输入错误,请重新输入!");
}
// 登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 调用权限框架方法获取用户名
// passwordEncoder参数:第一个用户传过来的密码,第二个从 userDetails 中获取的密码
if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
return RespBean.error("用户名或密码不正确");
}
if (!userDetails.isEnabled()) {
return RespBean.error("账号被禁用,请联系管理员!");
}
// 更新 security 登录用户对象,设置到全局
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails
, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 登录成功,生成 token
String token = jwtTokenUtil.generateToken(userDetails);
Map tokenMap = new HashMap<>();
tokenMap.put("tokenHead", tokenHead);
tokenMap.put("token", token);
return RespBean.success("登录成功", tokenMap);
}
/**
* 根据用户名获取用户
*
* @param username
* @return
*/
@Override
public Admin getAdminByUserName(String username) {
// 要作空判断,这里为了简单直接返回
return adminMapper.selectOne(new QueryWrapper().eq("username", username).eq("enabled", true));
}
}
(三)测试接口
二.菜单功能实现
(一)根据用户id查询菜单列表
1.修改菜单实体类(在菜单类里添加子菜单属性)
修改yeb/yeb-server/src/main/java/com/cxy/server/pojo/Menu.java
文件
2.修改菜单接口
修改:yeb/yeb-server/src/main/java/com/cxy/server/controller/MenuController.java
文件
package com.cxy.server.controller;
import com.cxy.server.pojo.Menu;
import com.cxy.server.service.IMenuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
*
* 前端控制器
*
*
* @author 陈鑫元
* @since 2021-05-21
*/
@RestController
@RequestMapping("/system/cfg")
public class MenuController {
@Autowired
private IMenuService menuService;
@ApiOperation(value = "通过用户id查询菜单列表")
@GetMapping("/menu")
public List
3.修改菜单Service
修改:yeb/yeb-server/src/main/java/com/cxy/server/service/IMenuService.java
文件
package com.cxy.server.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cxy.server.pojo.Menu;
import java.util.List;
/**
*
* 服务类
*
*
* @author 陈鑫元
* @since 2021-05-21
*/
public interface IMenuService extends IService
4.修改菜单Service实现类以实现根据管理员id查询菜单列表功能
修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/MenuServiceImpl.java
文件
package com.cxy.server.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cxy.server.mapper.MenuMapper;
import com.cxy.server.pojo.Admin;
import com.cxy.server.pojo.Menu;
import com.cxy.server.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
*
* 服务实现类
*
*
* @author 陈鑫元
* @since 2021-05-21
*/
@Service
public class MenuServiceImpl extends ServiceImpl implements IMenuService {
@Autowired
private MenuMapper menuMapper;
/**
* 通过用户id查询菜单列表
*
* @return
*/
@Override
public List getMenusByAdminId() {
return menuMapper.getMenusByAdminId(((Admin) SecurityContextHolder
.getContext().getAuthentication().getPrincipal()).getId());
}
}
5.修改菜单Mapper
修改:yeb/yeb-server/src/main/java/com/cxy/server/mapper/MenuMapper.java
文件
package com.cxy.server.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cxy.server.pojo.Menu;
import java.util.List;
/**
*
* Mapper 接口
*
*
* @author 陈鑫元
* @since 2021-05-21
*/
public interface MenuMapper extends BaseMapper {
/**
* 通过用户id查询菜单列表
*
* @param id
* @return
*/
List getMenusByAdminId(Integer id);
}
6.修改xml中的sql语句
修改:yeb/yeb-server/src/main/resources/mapper/MenuMapper.xml
文件
id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
(二)测试接口
(三)使用Redis存放菜单数据
1.配置Redis工具类
新建yeb/yeb-server/src/main/java/com/cxy/server/config/RedisConfig.java
文件:
package com.cxy.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author 陈鑫元
* @description Redis 配置类
* @date 2021-05-26 12:01
* @since 1.0.0
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
// String 类型 Key 序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());
// Json 类型 Value 序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// String 类型 HashKey 序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Json 类型 HashValue 序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
2.修改菜单Service实现类从Redis存取数据
修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/MenuServiceImpl.java
文件
package com.cxy.server.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cxy.server.mapper.MenuMapper;
import com.cxy.server.pojo.Admin;
import com.cxy.server.pojo.Menu;
import com.cxy.server.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
*
* 服务实现类
*
*
* @author 陈鑫元
* @since 2021-05-21
*/
@Service
public class MenuServiceImpl extends ServiceImpl implements IMenuService {
@Autowired
private MenuMapper menuMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 通过用户id查询菜单列表
*
* @return
*/
@Override
public List getMenusByAdminId() {
Integer adminId = ((Admin) SecurityContextHolder
.getContext().getAuthentication().getPrincipal()).getId();
ValueOperations valueOperations = redisTemplate.opsForValue();
// 从 redis 获取菜单数据
@SuppressWarnings("unchecked")
List menus = (List) valueOperations.get("menu_" + adminId);
// 如果为空,去数据库获取
if (CollectionUtils.isEmpty(menus)) {
menus = menuMapper.getMenusByAdminId(adminId);
// 将数据设置到 Redis 中
valueOperations.set("menu_" + adminId, menus);
}
return menus;
}
}
3.测试
第一次查询时,Redis
并没有菜单数据
会从数据库中查询菜单数据并设置到
Redis
中,此时再次查看发现Redis
中已经有数据。再次查询会直接查询Redis
中数据。
三.角色功能实现
(一)权限管理
1.权限管理RBAC基本概念
RBAC
是基于角色的访问控制( Role-Based Access Control
)在RBAC
中,权限与角色相关联,用户通过扮演适当的角色从而得到这些角色的权限。这样管理都是层级相互依赖的,权限赋予给角色,角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
RBAC授权实际上是 Who
、What
、 How
三元组之间的关系,也就是 Who
对 What
进行 How
的操作,简单说明就是谁对什么资源做了怎样的操作。
2.RBAC表结构设计
实体对应关系
用户-角色-资源实体间对应关系图分析如下:
这里用户与角色实体对应关系为多对多,角色与资源对应关系同样为多对多关系,所以在实体设计上用户与角色间增加用户角色实体,将多对多的对应关系拆分为一对多,同理,角色与资源多对多对应关系拆分出中间实体对象权限实体。
3.表结构设计
从上面实体对应关系分析,权限表设计分为以下基本的五张表结构:用户表(admin
),角色表(role
),用户角色表(admin_role
),菜单表(menu
),菜单权限表(menu_role
),表结构关系如下:
(二)根据请求url判断角色
1.修改菜单实体类(在菜单类里添加角色列表属性)
修改yeb/yeb-server/src/main/java/com/cxy/server/pojo/Menu.java
文件
2.修改菜单Service
修改:yeb/yeb-server/src/main/java/com/cxy/server/service/IMenuService.java
文件
3.修改菜单Service实现类
修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/MenuServiceImpl.java
文件
4.修改菜单Mapper
修改:yeb/yeb-server/src/main/java/com/cxy/server/mapper/MenuMapper.java
文件
5.修改菜单xml
修改:yeb/yeb-server/src/main/java/mapper/MenuMapper.xml
文件
id, url, path, component, name, iconCls, keepAlive, requireAuth, parentId, enabled
(二)判断用户登录角色
判断登录的用户都有那些角色,并和跟据请求url
判断角色进行比较,如果有一致的,则是合法访问(用户可以访问此菜单资源),否则为非法访问(用户不可以访问此菜单资源)。
1.修改管理员实体类
修改:yeb/yeb-server/src/main/java/com/cxy/server/pojo/Admin.java
文件
package com.cxy.server.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* 管理员表
*
*
* @author 陈鑫元
* @since 2021-05-21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_admin")
@ApiModel(value = "Admin对象", description = "管理员表")
public class Admin implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "姓名")
private String name;
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "住宅电话")
private String telephone;
@ApiModelProperty(value = "联系地址")
private String address;
@ApiModelProperty(value = "是否启用")
@Getter(AccessLevel.NONE) // 不需要生成 get 方法,防止与 UserDetails 重写的 isEnabled 冲突
private Boolean enabled;
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "用户头像")
private String userFace;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "角色")
@TableField(exist = false)
private List roles;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List authorities = roles
.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
2.修改管理员Service
修改:yeb/yeb-server/src/main/java/com/cxy/server/service/IAdminService.java
文件
3.修改管理员ServiceImpl
修改:yeb/yeb-server/src/main/java/com/cxy/server/service/impl/AdminServiceImpl.java
文件
4.修改角色Mapper
修改:yeb/yeb-server/src/main/java/com/cxy/server/mapper/RoleMapper.java
文件
5.修改角色xml
修改:yeb/yeb-server/src/main/resources/mapper/RoleMapper.xml
文件
id, name, nameZh
6.修改登录接口和Security配置文件
在登录和获取用户信息方法中添加getRoles()
方法,登录和获取用户信息时能得到角色列表。
修改:yeb/yeb-server/src/main/java/com/cxy/server/controller/LoginController.java
文件
修改:
yeb/yeb-server/src/main/java/com/cxy/server/config/security/SecurityConfig.java
文件
7.添加过滤器判断用户的角色
新建:yeb/yeb-server/src/main/java/com/cxy/server/config/security/component/CustomUrlDecisionManager.java
文件
package com.cxy.server.config.security.component;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* @author 陈鑫元
* 权限控制
* @description 判断用户角色
* @date 2021-05-27 11:17
* @since 1.0.0
*/
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection ConfigAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : ConfigAttributes) {
// 当前 url 所需角色
String needRole = configAttribute.getAttribute();
// 判断角色是否是登录即可访问的角色,此角色在在 CustomFilter 中设置
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录!");
} else {
return;
}
}
// 判断用户角色是否为 url 所需角色
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return false;
}
@Override
public boolean supports(Class> aClass) {
return false;
}
}
8.配置Security,添加动态权限控制
修改:yeb/yeb-server/src/main/java/com/cxy/server/config/security/SecurityConfig.java
文件
(三)测试接口
修改:yeb/yeb-server/src/main/java/com/cxy/server/controller/HelloController.java
文件,添加两个测试接口
package com.cxy.server.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 陈鑫元
* @description 测试接口
* @date 2021-05-23 17:22
* @since 1.0.0
*/
@Api(tags = "HelloController")
@RestController
public class HelloController {
@ApiOperation(value = "测试接口")
@GetMapping("/hello")
public String hello() {
return "hello";
}
@ApiOperation(value = "测试接口2")
@GetMapping("/employee/basic/hello")
public String hello2() {
return "/employee/basic/hello";
}
@ApiOperation(value = "测试接口3")
@GetMapping("/employee/advanced/hello")
public String hello3() {
return "/employee/advanced/hello";
}
}
运行结果:
测试接口2:
测试接口3: