大家可以直接微信扫描上面的二维码关注我的公众号,然后回复【bg25】 里面就会给到源代码的下载地址同时会附上相应的视频教程,并定期在我的公众号上给大家推送相应的技术文章,欢迎大家关注我的公众号。
在第二十四章我们已经实现了我们的整个权限架构了,但是我们还没有和我们整个系统的流程结合起来,那么在本章我们将结合登录将我们整个鉴权流程给串联起来,首先在上一章我们虽然引入了redis但是我们没有将redis的配置配置上去因此我们在本章将redis的配置补充上去
首先打开我们的application-dev.yml增加如下的配置:
spring:
redis:
host: 127.0.0.1
password: 123456
port: 6379
database: 0
jedis:
pool:
max-active: 8
max-idle: 20
min-idle: 0
timeout: 2000
接着打开我们的主入口类BgAdminWebCoreApplication增加redis的bean的注入:
@Bean
RedisCache redisCache(){
return new RedisCache();
}
由于我们使用了redis保存用户的信息因此用户信息我们需要进行序列化,因此我们需要将User.java进行序列化,序列化部分代码如下:
@Table(name = "t_user")
public class User implements Serializable {
private static final long serialVersionUID = -5809782578282943998L;
......
}
首先用户登录成功以后我们需要根据当前登录用户的ID来获取用户所拥有的菜单数据,因此需要在TreeDao中增加getLoginUserTree方法,代码如下:
/**
* 功能描述:根据用户登录的ID来获取菜单节点的数据
* @param userId 用户ID
* @return 返回数据集合
*/
List getLoginUserTree(@Param("userId") String userId);
接着配置TreeDao.xml文件,新增的代码如下:
!-- 根据用户登录的ID来获取菜单节点的数据 -->
当用户登录成功以后会获取到一个token,此时前端直接通过这个token来获取相关的用户信息,因此我们在UserDao中增加了一个根据token来获取用户信息的方法【getUserInfo】代码如下:
/**
* 功能描述:根据token来获取用户数据
* @param token token的值
* @return 返回获取的结果
*/
User getUserInfo(@Param("token")String token);
接着配置UserDao.xml新增的代码如下:
用户登录和获取用户信息的逻辑实现的代码如下:
package com.github.bg.admin.core.service;
import com.github.bg.admin.core.auth.Auth;
import com.github.bg.admin.core.constant.SystemStaticConst;
import com.github.bg.admin.core.dao.TreeDao;
import com.github.bg.admin.core.dao.UserDao;
import com.github.bg.admin.core.entity.ReturnInfo;
import com.github.bg.admin.core.entity.Tree;
import com.github.bg.admin.core.entity.User;
import com.github.bg.admin.core.util.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/**
* @author linzf
* @since 2019/07/05
* 类描述:用户的service类的实现
*/
@Service
@Transactional(rollbackFor = {IllegalArgumentException.class})
public class UserService {
@Autowired
private UserDao userDao;
@Autowired
private TreeDao treeDao;
/**
* 鉴权的实现
*/
@Autowired
private Auth authProvider;
/**
* 功能描述:实现用户登陆
*
* @param loginAccount 用户账号
* @param loginPassword 用户密码
* @return 返回登陆结果
*/
public ReturnInfo login(String loginAccount, String loginPassword) {
return authProvider.login(loginAccount, loginPassword);
}
/**
* 功能描述:根据token来获取用户数据
*
* @param token token的值
* @return 返回获取的结果
*/
public ReturnInfo getUserInfo(String token) {
User user = userDao.getUserInfo(token);
if (user == null) {
return new ReturnInfo(SystemStaticConst.FAIL, "获取账号信息错误");
}
user.setLoginPassword(null);
List treeList = treeDao.getLoginUserTree(user.getUserId());
String[] access = new String[treeList.size()];
for (int i = 0; i < treeList.size(); i++) {
access[i] = treeList.get(i).getTreeCode();
}
Map result = JsonUtils.objToMap(user);
result.put("access", access);
return new ReturnInfo(SystemStaticConst.SUCCESS, "登陆成功", result);
}
}
用户登录和获取用户信息的响应的实现的代码如下:
package com.github.bg.admin.core.controller;
import com.github.bg.admin.core.constant.SystemStaticConst;
import com.github.bg.admin.core.entity.ReturnInfo;
import com.github.bg.admin.core.service.UserService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* @author linzf
* @since 2019/5/15
* 类描述:用户的controller
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 功能描述:根据token来获取用户数据
*
* @param token token的值
* @return 返回获取的结果
*/
@ApiOperation(value = "根据token来获取用户数据")
@PostMapping("getUserInfo")
public ReturnInfo getUserInfo(@RequestParam(name = "token") String token) {
return userService.getUserInfo(token);
}
/**
* 功能描述:实现用户登陆
*
* @param loginAccount 用户账号
* @param loginPassword 用户密码
* @return 返回登陆结果
*/
@ApiOperation(value = "实现用户登陆")
@PostMapping("login")
public ReturnInfo login(@RequestParam(name = "loginAccount") String loginAccount, @RequestParam(name = "loginPassword") String loginPassword) {
return userService.login(loginAccount, loginPassword);
}
}
上面已经实现了我们登录和获取用户信息的逻辑过程了,但是我们并没有对我们的权限进行相应的拦截,因此我们需要在此处实现我们的拦截器,因此我们在core包底下创建一个filter包同时在该包底下创建一个过滤器【AuthInterceptor】在实现该过滤器之前我们需要实现WriteUtil工具类代码如下:
package com.github.bg.admin.core.util;
import com.github.bg.admin.core.entity.ReturnInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author linzf
* @since 2019/6/10
* 类描述:
*/
public class WriteUtil {
static Logger log = LoggerFactory.getLogger(WriteUtil.class);
/**
* 功能描述:异常信息处理
*
* @param httpServletResponse
* @param failCode 失败码
* @param msg 失败原因
*/
public static void write(HttpServletResponse httpServletResponse, int failCode, String msg) {
PrintWriter writer = null;
try {
/**
* 设置允许跨域请求和返回页面的编码
*/
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
writer = httpServletResponse.getWriter();
String result = JsonUtils.objToJson(new ReturnInfo(failCode, msg));
writer.print(result);
} catch (IOException e) {
log.error("response error", e);
} finally {
if (writer != null) {
writer.close();
}
}
}
}
拦截器AuthInterceptor的代码实现如下:
package com.github.bg.admin.core.filter;
import com.github.bg.admin.core.auth.ReleaseUrl;
import com.github.bg.admin.core.constant.SystemStaticConst;
import com.github.bg.admin.core.entity.User;
import com.github.bg.admin.core.util.RedisCache;
import com.github.bg.admin.core.util.StringUtils;
import com.github.bg.admin.core.util.WriteUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author linzf
* @since 2019-05-14
* 类描述:拦截器实现权限的拦截
*/
public class AuthInterceptor implements HandlerInterceptor {
private Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
/**
* 操作redis的实现类
*/
@Autowired
private RedisCache redisCache;
/**
* 获取放行的权限的接口
*/
@Autowired
private ReleaseUrl releaseUrl;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
String token = httpServletRequest.getHeader("x-access-token");
String actionUrl = httpServletRequest.getServletPath();
if (SystemStaticConst.ACTION_TYPE_OPTIONS.equals(httpServletRequest.getMethod())) {
return true;
}
/**
* 判断当前的响应地址是否可以放行
*/
String releasePath = releaseUrl.getReleaseUrl();
if (releasePath.indexOf(actionUrl) == -1) {
if (token == null || "".equals(token) || "null".equals(token)) {
WriteUtil.write(httpServletResponse, SystemStaticConst.NOT_LOGIN, "用户未登录");
return false;
}
/**
* 判断当前的用户是否有权限访问当前节点
*/
log.debug("token:{}", token);
String powerPath = StringUtils.getObjStr(redisCache.getString(token));
if ("".equals(powerPath)) {
WriteUtil.write(httpServletResponse, SystemStaticConst.AUTH_TOKEN_EXPIRE, "token过期");
return false;
}
if (powerPath.indexOf(actionUrl) == -1) {
WriteUtil.write(httpServletResponse, SystemStaticConst.AUTH_FAIL, "用户无权限,请联系系统管理员");
return false;
}
/**
* 根据token来获取当前登录的用户的信息
*/
User user = redisCache.getObject(token + "_USER", User.class);
if (user == null) {
WriteUtil.write(httpServletResponse, SystemStaticConst.NOT_LOGIN, "用户未登录");
return false;
}
}
return true;
}
}
上面我们虽然编写好了拦截器可是拦截器并没有生效,因此我们需要配置拦截器让拦截器生效因此我们在config目录底下创建一个InterceptorConfig代码如下:
package com.github.bg.admin.core.config;
import com.github.bg.admin.core.filter.AuthInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author linzf
* @since 2019-06-04
* 类描述:设置拦截器
*/
@Component
public class InterceptorConfig implements WebMvcConfigurer {
/**
* 初始化鉴权拦截器
*/
private AuthInterceptor authInterceptor = new AuthInterceptor();
/**
* 注入权限拦截器bean
*/
@Bean
AuthInterceptor authInterceptor(){
return authInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 先加的拦截器先执行
registry.addInterceptor(authInterceptor()).addPathPatterns("/**");
}
}
由于我们之前在编写前端的时候一直采用的是mock数据的形式来进行调用,但是我们现在需要改造成前后端交互了,因此我们需要改造lib底下的【axios.js】、api底下的【base.js】以及config底下的【env.js】改造完成以后的代码如下:
import Axios from 'axios';
import {Message} from 'iview';
import config from '../config/run.config';
import userStore from '../store/module/user';
import qs from 'qs';
class httpRequest {
constructor() {
this.options = {
method: '',
url: ''
};
// 存储请求队列
this.queue = [];
}
// 销毁请求实例
destroy(url) {
delete this.queue[url];
const queue = Object.keys(this.queue);
return queue.length;
}
// 请求拦截
interceptors(instance, url) {
// 添加请求拦截器
instance.interceptors.request.use(config => {
if (userStore.state.token != '') {
config.headers['x-access-token'] = userStore.state.token;
}
return config
}, error => {
// 对请求错误做些什么
return Promise.reject(error);
})
// 添加响应拦截器
instance.interceptors.response.use((res) => {
let {data} = res;
// 表示当前的操作的用户还没有登录,因此重新跳转到登录页面
if (data.code == 401) {
userStore.state.token = '';
window.location.href = window.location.pathname + '#/login';
Message.error('未登录,或登录失效,请登录');
} else if (data.code == 409) {
// token过期重新刷新token的值
this.refreshToken();
}
return data;
}, (error) => {
Message.error('服务内部错误');
// 对响应错误做点什么
return Promise.reject(error);
})
}
// 创建实例
create() {
let conf = {
baseURL: config.runConfig.baseUrl,
timeout: 5000,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'X-URL-PATH': location.pathname
}
};
return Axios.create(conf);
}
refreshToken() {
Axios.post(config.runConfig.refreshTokenUrl, qs.stringify({refreshToken: userStore.state.refreshToken}))
.then(response => {
let {data} = response;
// 表示可以重新发起请求
if (data.code == 200) {
userStore.state.token = data.obj.token;
userStore.state.refreshToken = data.obj.refreshToken;
localStorage.setItem('token', data.obj.token);
localStorage.setItem('refreshToken', data.obj.refreshToken);
} else {
userStore.state.token = '';
userStore.state.refreshToken = '';
localStorage.setItem('token', '');
localStorage.setItem('refreshToken', '');
window.location.href = window.location.pathname + '#/login';
Message.error('登录失效,请登录');
}
})
.catch((error) => {
console.log('error=>' + error)
})
}
// 请求实例
request(options) {
let instance = this.create();
this.interceptors(instance, options.url);
options = Object.assign({}, options);
this.queue[options.url] = instance;
return instance(options);
}
}
export default httpRequest;
import axios from '../lib/api.request';
import qs from 'qs';
import config from '../config/run.config';
let Axios
if (config.runConfig.mock) {
Axios = require('axios');
Axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
}
// 是否需要刷新token的标志,防止在刷新token的时候,发送的请求到后端导致最后退出系统
let isRefresh = false;
export function fetch(url, params = {}) {
if (config.runConfig.mock) {
return new Promise((resolve, reject) => {
Axios.post(url, params)
.then(response => {
resolve(response.data);
})
.catch((error) => {
reject(error);
})
})
} else {
return new Promise((resolve, reject) => {
// 如果是在刷新token,那么这时候其他请求将会被500毫秒以后再次触发
if(isRefresh){
setTimeout(()=>{
fetch(url,params);
},1000)
}else{
axiosPost(url,params,resolve)
}
});
}
}
// 递归调用,保证在token过期刷新token的时候可以实现请求的二次发送
function axiosPost(url,params,resolve){
axios.request({
url: url,
data: qs.stringify(params),
method: 'post'
}).then(res => {
if (res.code == 409) {
isRefresh = true;
setTimeout(() => {
isRefresh = false;
axiosPost(url,params,resolve)
}, 1000)
}else{
resolve(res);
}
})
}
export default "development";
那么到此处我们就已经完成了登录的整个改造了,我们需要分别启动相应的程序来验证我们的效果了,首先我们需要启动我们的redis数据库、接着启动我们的后端工程、最后启动我们的前端工程,然后直接访问http://127.0.0.1:8080/#/login
接着我们输入账号admin密码123456,我们会看到如下的两个请求,同时会跳转到首页。
但是我们现在点击上面的任何一个菜单节点都会报错,这是正常的因为现在我们所有的请求都转到了后端,因此我们需要在下面的章节开始编写相应的接口。
上一篇文章地址:spring boot+iview 前后端分离架构之后端鉴权体系的实现(二十四)
下一篇文章地址:spring boot+iview 前后端分离架构之数据字典的实现(二十六)