如题,实现思路如下:
-
定义用户,角色,权限,三者关联的实体类,定义用户-数据源的实体类。
-
定义各自的Repository,继承JpaRepository。spring jpa本身会自动生成标准的增删改查的方法供使用,也可以按规则定义类似
findFirstByUserName(String userName);
的方法,不需要自己实现,spring jpa会自动实现该方法。更详细的参考Spring Data Jpa的使用
-
服务启动后,从数据库取出用户/权限等相关数据,放入Redis,后续用户/权限等操作均从Redis获取数据校验,再往后实现用户管理/权限分配等功能时去修改Redis的数据。
-
定义service供controller调用,用户登录时,通过JWT生成token返回给前端。
主要参考: 集成JWT实现token验证 , 使用JWT做用户登陆token校验
-
对除了/login的其他请求进行拦截,校验token,从token解析出用户名,校验用户权限,切换到用户使用的数据源。
代码粗略实现如下:
先 编写 抄袭一个类,用于返回统一数据格式给前端:
以下放入core模块
public class JsonResult extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public static final String CODE_TAG = "code";
public static final String MSG_TAG = "msg";
public static final String DATA_TAG = "data";
/**
* 状态类型
*/
public enum Type
{
/** 成功 */
SUCCESS(1),
/**用户没有授权 */
FAIL(0),
/** 权限不足 */
WARN(403),
/** 错误 */
ERROR(500);
private final int value;
Type(int value)
{
this.value = value;
}
public int value()
{
return this.value;
}
}
/** 状态类型 */
private Type type;
/** 状态码 */
private int code;
/** 返回内容 */
private String msg;
/** 数据对象 */
private Object data;
/**
* 初始化一个新创建的 JsonResult 对象,使其表示一个空消息。
*/
public JsonResult()
{
}
/**
* 初始化一个新创建的 JsonResult 对象
*
* @param type 状态类型
* @param msg 返回内容
*/
public JsonResult(Type type, String msg)
{
super.put(CODE_TAG, type.value);
super.put(MSG_TAG, msg);
}
/**
* 初始化一个新创建的 JsonResult 对象
*
* @param type 状态类型
* @param msg 返回内容
* @param data 数据对象
*/
public JsonResult(Type type, String msg, Object data)
{
super.put(CODE_TAG, type.value);
super.put(MSG_TAG, msg);
super.put(DATA_TAG, data);
}
/**
* 返回成功消息
*
* @return 成功消息
*/
public static JsonResult success()
{
return JsonResult.success("操作成功");
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static JsonResult success(String msg)
{
return JsonResult.success(msg, null);
}
/**
* 返回成功消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static JsonResult success(String msg, Object data)
{
return new JsonResult(Type.SUCCESS, msg, data);
}
/**
* 返回失败消息
*
* @return 成功消息
*/
public static JsonResult fail()
{
return JsonResult.fail("没有授权访问");
}
/**
* 返回失败消息
*
* @param msg 返回内容
* @return 成功消息
*/
public static JsonResult fail(String msg)
{
return JsonResult.fail(msg, null);
}
/**
* 返回失败消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 成功消息
*/
public static JsonResult fail(String msg, Object data)
{
return new JsonResult(Type.FAIL, msg, data);
}
public static JsonResult warn()
{
return JsonResult.warn("权限不足");
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @return 警告消息
*/
public static JsonResult warn(String msg)
{
return JsonResult.warn(msg, null);
}
/**
* 返回警告消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 警告消息
*/
public static JsonResult warn(String msg, Object data)
{
return new JsonResult(Type.WARN, msg, data);
}
/**
* 返回错误消息
*
* @return
*/
public static JsonResult error()
{
return JsonResult.error("操作失败");
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @return 错误消息
*/
public static JsonResult error(String msg)
{
return JsonResult.error(msg, null);
}
/**
* 返回错误消息
*
* @param msg 返回内容
* @param data 数据对象
* @return 错误消息
*/
public static JsonResult error(String msg, Object data)
{
return new JsonResult(Type.ERROR, msg, data);
}
public Type getType()
{
return type;
}
public void setType(Type type)
{
this.type = type;
}
public int getCode()
{
return code;
}
public void setCode(int code)
{
this.code = code;
}
public String getMsg()
{
return msg;
}
public void setMsg(String msg)
{
this.msg = msg;
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
}
编写 抄袭两个使用到的工具类:
以下放入common模块
public class CodecUtil {
/**
* 将字符串 MD5 加密
*/
public static String encryptMd5(String str) {
return DigestUtils.md5Hex(str);
}
}
public class JwtUtil {
/**
* 过期时间为12小时
*/
private static final long EXPIRE_TIME = 12*60*60*1000;
/**
* token私钥
*/
private static final String TOKEN_SECRET = "jfiejf23自己随便写点2fy2fuf3f";
/**
* 生成签名
* @param username
* @param password
* @return
*/
public static String sign(String username, String password){
//过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
//私钥及加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
//设置头信息
HashMap<String, Object> header = new HashMap<>(2);
header.put("typ", "JWT");
header.put("alg", "HS256");
//附带username和userID生成签名
return JWT.create()
.withHeader(header)
.withClaim("username",username)
.withClaim("password",password)
.withExpiresAt(date)
.sign(algorithm);
}
/**
* 校验token 成功返回用户名
* @param token
* @return
*/
public static String verityAndGetUser(String token){
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
String userName = jwt.getClaim("username").asString();
return userName;
} catch (IllegalArgumentException e) {
return "";
} catch (JWTVerificationException e) {
return "";
}
}
}
以下放入dao模块
Entity(略去getter/setter):
用户:
@Entity
@Table(name = "sys_user")
public class SysUser {
@Id
private String userName;
private String passWord;
}
角色:
@Entity
@Table(name = "sys_role")
public class SysRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String roleCode;
private String roleName;
}
权限(permCode其实就是存放请求路径,如 /hello):
@Entity
@Table(name = "sys_perm")
public class SysPerm{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String permCode;
private String permName;
}
用户-角色 对照表:
@Entity
@Table(name = "sys_user_role")
public class SysUserRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String userName;
private long roleId;
}
角色-权限 对照表:
@Entity
@Table(name = "sys_role_perm")
public class SysRolePerm {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private long roleId;
private long permId;
}
用户-数据源 对照表:
@Entity
@Table(name = "sys_user_datasource")
public class SysUserCurrentDatasource {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String userName;
private String dataSourceId;
}
Repository:
(创建每个Entity对应的Repository,继承JpaRepository即可,只列出一个比较特殊的)
public interface SysRoleRepository extends JpaRepository<SysRole, Long>{
/**
* 根据用户名查找该用户所有权限
* @param userName
* @return List<SysRole>
*/
@Query(value = "select r.* from sys_role r, sys_user_role ur where ur.user_name = ?1 and ur.role_id = r.id", nativeQuery = true)
List<SysRole> findRolesOfUser(String userName);
/**
* 根据权限Id查找授权的角色
* @param permId
* @return List<SysRole>
*/
@Query(value = "select r.* from sys_role r, sys_role_perm rp where rp.perm_id = ?1 and rp.role_id = r.id", nativeQuery = true)
List<SysRole> findRolesOfPerm(long permId);
}
初始化Redis:
定义需要用到常量:
public interface RedisConstant {
/**
* 缓存保存时间
*/
long TIME_LONG = 30;
/**
* 时间类型
*/
TimeUnit TIME_UNIT = TimeUnit.DAYS;
/**
* 用户-密码
*/
String USER_TOKEN = "userToken-";
/**
* 用户-角色
*/
String USER_ROLES = "userRoles-";
/**
* 用户-数据源ID
*/
String USER_DATASOURCE = "userDatasource-";
/**
* 权限-角色
*/
String PERM_ROLE = "permRole-";
}
初始化Redis数据,代码仍需优化:
@Component
public class InitRedisData implements CommandLineRunner {
private static Logger logger = LoggerFactory.getLogger(InitRedisData.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private SysUserRepository userRepository;
@Autowired
private SysRoleRepository roleRepository;
@Autowired
private SysUserDataSourceRepository sysUserDataSourceRepository;
@Autowired
private SysPermRepository sysPermRepository;
@Override
public void run(String... args) throws Exception {
logger.info("开始初始化Redis数据.");
try {
List<SysUser> userList = userRepository.findAll();
for (SysUser user: userList) {
String userName = user.getUserName();
//用户账号密码
stringRedisTemplate.opsForValue().set(RedisConstant.USER_TOKEN + userName, user.getPassWord(), RedisConstant.TIME_LONG, RedisConstant.TIME_UNIT);
//用户拥有的角色
List<SysRole> sysRoles = roleRepository.findRolesOfUser(userName);
String roles = changeToString(sysRoles);
stringRedisTemplate.opsForValue().set(RedisConstant.USER_ROLES + userName, roles, RedisConstant.TIME_LONG, RedisConstant.TIME_UNIT);
}
//用户当前使用的数据库
List<SysUserCurrentDatasource> userDataSources = sysUserDataSourceRepository.findAll();
for (SysUserCurrentDatasource userDataSource: userDataSources) {
stringRedisTemplate.opsForValue().set(RedisConstant.USER_DATASOURCE + userDataSource.getUserName(), userDataSource.getDataSourceId(), RedisConstant.TIME_LONG, RedisConstant.TIME_UNIT);
}
//权限所需的角色列表
List<SysPerm> perms = sysPermRepository.findAll();
for (SysPerm perm: perms) {
List<SysRole> sysRoles = roleRepository.findRolesOfPerm(perm.getId());
String roles = changeToString(sysRoles);
stringRedisTemplate.opsForValue().set(RedisConstant.PERM_ROLE + perm.getPermCode(), roles, RedisConstant.TIME_LONG, RedisConstant.TIME_UNIT);
}
}catch (Exception e){
logger.error("初始化Redis数据失败:" + e.getMessage());
throw e;
}
logger.info("结束初始化Redis数据.");
}
private String changeToString(List<SysRole> sysRoles){
String roles = "";
if (sysRoles != null && sysRoles.size()>0){
StringBuilder sb = new StringBuilder();
for (SysRole role: sysRoles) {
sb.append(role.getRoleCode());
sb.append(",");
}
roles = sb.toString().substring(0, sb.toString().length()-1);
}
return roles;
}
}
写个Repository供其他地方调用Redis的数据:
@Repository
public class SystemInfoRedis {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 获取数据库用户对象(用户名和密码)
* @param userName
* @return
*/
public SysUser getByUserName(String userName){
SysUser sysUser = new SysUser();
String password = stringRedisTemplate.opsForValue().get(RedisConstant.USER_TOKEN + userName);
if (password != null && !password.isEmpty()){
sysUser.setUserName(userName);
sysUser.setPassWord(password);
}
return sysUser;
}
/**
* 获取用户所使用的数据源
* @param userName
* @return dsName
*/
public String getDataSourceByUserName(String userName){
return stringRedisTemplate.opsForValue().get(RedisConstant.USER_DATASOURCE + userName);
}
/**
* 根据用户名获取所拥有的角色
* @param userName
* @return admin,user,...
*/
public String getRolesByUserName(String userName){
String roles = stringRedisTemplate.opsForValue().get(RedisConstant.USER_ROLES + userName);
return roles == null ? "": roles;
}
/**
* 根据用户名获取所拥有的角色列表
* @param userName
* @return List<String>
*/
public List<String> getRoleListByUserName(String userName){
List<String> roleList = new ArrayList<>();
String roles = stringRedisTemplate.opsForValue().get(RedisConstant.USER_ROLES + userName);
if (roles != null && !"".equals(roles)) {
String[] roleArray = roles.split(",");
roleList = Arrays.asList(roleArray);
}
return roleList;
}
/**
* 根据权限,获取需要的角色信息
* @param permCode
* @return admin,user,...
*/
public String getRolesByPermCode(String permCode){
String roles = stringRedisTemplate.opsForValue().get(RedisConstant.PERM_ROLE + permCode);
return roles == null ? "": roles;
}
/**
* 根据权限,获取需要的角色列表
* @param permCode
* @return List<String>
*/
public List<String> getRoleListByPermCode(String permCode){
List<String> roleList = new ArrayList<String>();
String roles = stringRedisTemplate.opsForValue().get(RedisConstant.PERM_ROLE + permCode);
if (roles != null && !"".equals(roles)){
String[] roleArray = roles.split(",");
roleList = Arrays.asList(roleArray);
}
return roleList;
}
}
用户登录用的Service(数据库存放的是加密过的密码):
以下放入service模块
@Service
public class UserService {
@Autowired
private SystemInfoRedis systemInfoRedis;
/**
* 校验用户名和密码,成功返回TOKEN
* @param userName
* @param passWord
* @return token
*/
public String checkUser(String userName, String passWord) {
String token = "";
//获取Redis中的用户数据
SysUser user = systemInfoRedis.getByUserName(userName);
if ((user != null) && (!"".equals(user.getUserName()))){
//校验密码
if (CodecUtil.encryptMd5(passWord).equals(user.getPassWord())){
token = JwtUtil.sign(userName, passWord);
if (token != null) {
return token;
}
};
}
return token;
}
}
用户登录访问的Controller:
以下放入web模块
@RestController
public class LoginController {
@Autowired
private UserService userService;
@PostMapping(value = "/login")
@ResponseBody
public JsonResult login (@RequestBody Map<String,String> map){
String userName = map.get("userName");
String passWord = map.get("passWord");
//身份验证
String token = userService.checkUser(userName,passWord);
if (!"".equals(token)) {
return JsonResult.success("成功", token);
}
return JsonResult.fail("用户名及密码错误");
}
}
写个拦截器拦截请求:
主要实现校验请求头的token,校验用户权限,切换数据源。
将自定义的CurrentUser对象(存放用户信息)放入Request中,方便后续业务处理时使用。
public class CurrentUser {
private String token;
private String userName;
private String dataSourceId;
private List<String> roles;
...
}
@Component
public class RequestInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestInterceptor.class);
@Autowired
private SystemInfoRedis systemInfoRedis;
/**
* 进入controller层之前拦截请求
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
//获取请求头的token, 没有的化返回失败,前端需要登录
String token = request.getHeader("token");
if (token == null || "".equals(token)){
returnJson(response, JSON.toJSONString(JsonResult.fail()));
return false;
}
//校验token,过期等原因失败则前端需要重新登录
String userName = JwtUtil.verityAndGetUser(token);
if ("".equals(userName)) {
returnJson(response, JSON.toJSONString(JsonResult.fail()));
return false;
}
//判断权限,失败的话返回警告
String url = request.getServletPath();
List<String> needRoleList = systemInfoRedis.getRoleListByPermCode(url);
List<String> userRoleList = systemInfoRedis.getRoleListByUserName(userName);
for (String needRole : needRoleList){
if (!userRoleList.contains(needRole)){
returnJson(response, JSON.toJSONString(JsonResult.warn()));
return false;
}
}
//设置用户数据源
String dataSourceId = "";
if (userName != null)
{
dataSourceId = systemInfoRedis.getDataSourceByUserName(userName);
if (dataSourceId != null && !dataSourceId.isEmpty()){
DynamicDataSourceContextHolder.removeDataSourceRouterKey();
DynamicDataSourceContextHolder.setDataSourceRouterKey(dataSourceId);
}
}
//将用户信息对象通过setAttribute放入Request中
CurrentUser currentUser = new CurrentUser();
currentUser.setToken(token);
currentUser.setUserName(userName);
currentUser.setDataSourceId(dataSourceId);
currentUser.setRoles(userRoleList);
request.setAttribute("currentUser", currentUser);
return true;
}
private void returnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
log.error("拦截器返回JSON失败", e);
} finally {
if (writer != null){
writer.close();
}
}
}
/**
* 处理请求完成后视图渲染之前的处理操作
* @param httpServletRequest
* @param httpServletResponse
* @param o
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
DynamicDataSourceContextHolder.removeDataSourceRouterKey();
}
/**
* 视图渲染之后的操作
* @param httpServletRequest
* @param httpServletResponse
* @param o
* @param e
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
配置使拦截器生效,拦截除了/login外的请求:
@Configuration
public class WebAppConfig implements WebMvcConfigurer{
@Autowired
private RequestInterceptor requestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
至此,用户权限的功能基本实现,后续进行测试。