手把手教你前后分离架构 系统认证鉴权实现
Posted dehuisun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你前后分离架构 系统认证鉴权实现相关的知识,希望对你有一定的参考价值。
前面我们实现了前后分离项目基础的数据交互以及前端数据展示。用户登录部分一直是模拟登录,今天我们实现系统的身份认证部分让系统不在裸奔。
1、系统认证授权
认证就是要核验用户的身份,比如说通过用户名和密码来检验用户的身份。说简单一些,认证就是登陆。登陆之后Shiro要记录用户成功登陆的凭证。
授权是比认证更加精细度的划分用户的行为。比如说一个教务管理系统中,学生登陆之后只能查看信息,不能修改信息。而班主任就可以修改学生的信息。这就是利用授权来限定不同身份用户的行为。
安全是应用中不可缺少的功能,相较于其他认证与授权框架,Shiro设计的非常简单,所以广受好评。任意JavaWeb项目都可以使用Shiro框架。
我们采用shiro + JWT架构来实现系统安全认证。只使用jwt只能实现基础验证功能,所以我们把token存储再redis中,利用redis我们可以实现token踢出、token刷新等功能。
Shiro可以利用HttpSession或者Redis存储用户的登陆凭证,以及角色或者身份信息。然后利用过滤器(Filter),对每个Http请求过滤,检查请求对应的HttpSession或者Redis中的认证与授权信息。如果用户没有登陆,或者权限不够,那么Shiro会向客户端返回错误信息。
JWT(Json Web Token), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
如果用户的登陆凭证经过加密(Token)保存在客户端,客户端每次提交请求的时候,把Token上传给后端服务器节点。即便后端项目使用了负载均衡,每个后端节点接收到客户端上传的Token之后,经过检测,是有效的Token,于是就断定用户已经成功登陆,接下来就可以提供后端服务了。
传统的HttpSession依靠浏览器的Cookie存放SessionId,所以要求客户端必须是浏览器。现在的JavaWeb系统,客户端可以是浏览器、APP、小程序,以及物联网设备。为了让这些设备都能访问到JavaWeb项目,就必须要引入JWT技术。JWT的Token是纯字符串,至于客户端怎么保存,没有具体要求。只要客户端发起请求的时候,附带上Token即可。所以像物联网设备,我们可以用SQLite存储Token数据。
认证流程
2、SpringBoot集成redis
redis是一个key-value
2.1、添加pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
2.2 添加启动类
@Configuration
public class RedisConfig
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory)
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
// key采用String的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// hash的key也采用String的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
// value序列化方式采用jackson
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// hash的value序列化方式采用jackson
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
2.3、配置文件
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=1000
spring.redis.database=0
3、SpringBoot集成shiro
3.1、添加依赖
<shiro.version>1.2.4</shiro.version>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>$shiro.version</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>$shiro.version</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>$shiro.version</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>$shiro.version</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
3.2、添加启动类
@Configuration
public class ShiroConfig
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager)
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/document.html", "anon");
filterChainDefinitionMap.put("/configuration/ui", "anon");
filterChainDefinitionMap.put("/swagger-resources", "anon");
filterChainDefinitionMap.put("/authentication/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/actuator/**", "anon");
filterChainDefinitionMap.put("/sys/login/login", "anon");
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("authc", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
@Bean("securityManager")
public SecurityManager securityManager(MyRealm myRealm)
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
3.3、shiro实现
3.3.1、MyRealm实现类
@Component
public class MyRealm extends AuthorizingRealm
@Autowired
private ISysUserService sysUserService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token)
return token instanceof JwtToken;
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
return new SimpleAuthorizationInfo();
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException
String token = (String) auth.getCredentials();
if (token == null)
throw new AuthenticationException("token为空!");
// 校验token有效性
Object user = this.checkToken(token);
return new SimpleAuthenticationInfo(user, token, getName());
/**
* 校验token的有效性
*/
public Object checkToken(String token) throws AuthenticationException
// 解密获得username,用于和数据库进行对比
String userId = JwtUtil.getUserId(token);
if (StringUtils.isBlank(userId))
throw new AuthenticationException("token无效");
SysUser sysUser = sysUserService.getById(userId);
if (sysUser == null)
throw new AuthenticationException("用户不存在!");
// 判断用户状态
if (Constant.no.equals(sysUser.getStatus()))
throw new AuthenticationException("账号已被锁定,请联系管理员!");
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, String.valueOf(sysUser.getId())))
throw new AuthenticationException("Token失效,请重新登录!");
return sysUser;
/**
* 刷新token
*/
public boolean jwtTokenRefresh(String token, String userId)
String cacheToken = (String) redisTemplate.opsForValue().get(Constant.CATCHE_TOKEN + token);
if (StringUtils.isNotEmpty(cacheToken))
// 校验token有效性
if (!JwtUtil.verify(cacheToken))
String newToken = JwtUtil.sign(userId);
// 设置超时时间
redisTemplate.opsForValue().set(Constant.CATCHE_TOKEN + token, newToken, JwtUtil.expire * 2, TimeUnit.SECONDS);
return true;
return false;
3.3.2、过滤器实现
public class JwtFilter extends BasicHttpAuthenticationFilter
/**
* 执行登录认证
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name()))
return true;
return super.isAccessAllowed(request, response, mappedValue);
/**
*认证失败回调方法
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response)
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
try
//处理登录失败的异常
Throwable throwable = e.getCause() == null ? e : e.getCause();
String json = JSONObject.toJSONString(Result.noAccess().info(throwable.getMessage()));
httpResponse.getWriter().print(json);
catch (Exception exp)
exp.printStackTrace();
return false;
@Override
protected AuthenticationToken createToken(ServletRequest request,
ServletResponse response)
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token))
return null;
return new JwtToken(token);
/**
* 处理未经身份验证的请求
*/
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if(StringUtils.isBlank(token))
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setContentType("application/json;charset=utf-8");
String json = JSONObject.toJSONString(Result.noAccess());
httpResponse.getWriter().print(json);
return false;
return executeLogin(request, response);
/**
* 获取请求中的token
*/
private String getRequestToken(HttpServletRequest httpRequest)
//从header中获取token
String token = httpRequest.getHeader(Constant.token);
//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)||("null").equals(token))
token = httpRequest.getParameter(Constant.token);
//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)||("null").equals(token))
Cookie[] cookies = httpRequest.getCookies();
if(cookies != null)
for(Cookie cookie : cookies)
if(cookie.getName().equals(Constant.token))
token = cookie.getValue();
return token;
3.3.3、JwtUtil工具类
@Slf4j
@ConfigurationProperties(prefix = "mir.jwt")
@Component
@Data
public class JwtUtil
private static String secret;
public static long expire;
/**
* 生成jwt token
*/
public static String sign(String userId)
//过期时间
Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
/**
* 校验token
*/
public static boolean verify(String token)
try
Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
return true;
catch (Exception e)
return false;
/**
* 获取userId
*/
public static String getUserId(String token)
try
Claims claims=Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claims.getSubject();
catch (ExpiredJwtException e)
Claims claims = e.getClaims();
return claims.getSubject();
catch (Exception e)
return null;
public String getSecret()
return secret;
public void setSecret(String secret)
JwtUtil.secret = secret;
public long getExpire()
return expire;
public void setExpire(long expire)
JwtUtil.expire = expire;
3.3.4、JwtToken实体
public class JwtToken implements AuthenticationToken
//密钥
private String token;
public JwtToken(String token)
this.token = token;
@Override
public Object getPrincipal()
return token;
@Override
public Object getCredentials()
return token;
3.3.5、自定义配置文件
mir.jwt.secret = 123456
mir.jwt.expire = 900
4、登录功能实现
4.1、后端登录接口实现
@ApiOperation(value = "账号密码方式登录")
@PostMapping("/login")
public Result login(@Valid LoginParam loginParam)throws Exception
try
User user=new User();
user.setAccount(loginParam.getAccount());
List<User> list = userService.list(new QueryWrapper<>(user));
if(list!=null&&list.size()>0)
user=list.get(0);
if(Constant.LOCK.equals(user.getStatus()))
if(user.getLockTime()!=null)
int min = (int) (System.currentTimeMillis() - user.getLockTime().getTime())/(1000 * 60);
if(min>=lock_time)
user.setLockTime(null);
user.setStatus(Constant.YES);
user.setErrNum(0);
userService.updateById(user);
if(Constant.YES.equals(user.getStatus()))
if(loginParam.getPassword().equals(user.getPassword()))
if(user.getErrNum()!=null&&user.getErrNum()>0)
user.setErrNum(0);
user.setLockTime(null);
userService.updateById(user);
String token= JwtUtil.sign(String.valueOf(user.getId()));
redisTemplate.opsForValue().set(Constant.CATCHE_TOKEN + token,token,JwtUtil.expire * 2, TimeUnit.SECONDS);
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
Cookie cookie = new Cookie("token", token);
cookie.setMaxAge(604800);
cookie.setHttpOnly(true);
cookie.setPath("/");
response.addCookie(cookie);
return Result.ok().info(token);
else
Integer errNum=0;
if(user.getErrNum()!=null)
errNum=user.getErrNum()+1;
else
errNum=errNum+1;
if(errNum>=max_err_num)
user.setLockTime(new Date());
user.setStatus(Constant.LOCK);
else
user.setErrNum(errNum);
userService.updateById(user);
return Result.error().message("用户密码错误,再错误"+(max_err_num-errNum)+"次,用户将锁定");
else if(Constant.LOCK.equals(user.getStatus()))
return Result.error().message("账号已锁定");
else
return Result.error().message("账号已停用");
else
return Result.error().message("账号或密码错误");
catch (Exception e)
log.error("登录异常", e);
return Result.error().message("登录失败");
4.2、前端统一请求
4.2.1、请求方法封装
创建http工具类
import Vue from 'vue'
import axios from 'axios'
import router from '@/router'
import qs from 'qs'
import merge from 'lodash/merge'
import clearLoginInfo from '@/utils'
const http = axios.create(
timeout: 1000 * 3,
withCredentials: true,
headers:
'Content-Type': 'application/json; charset=utf-8'
)
/**
* 请求拦截
*/
http.interceptors.request.use(config =>
config.headers['token'] = Vue.cookie.get('token') // 请求头带上token
return config
, error =>
return Promise.reject(error)
)
/**
*
* 响应拦截
*/
http.interceptors.response.use(response =>
if (response.data.code === 401) // 401, token失效
clearLoginInfo()
router.push( name: 'login' ,params: data: response.data.messages )
return response
, error =>
return Promise.reject(error)
)
/**
* 请求地址处理
* @param * actionName action方法名称
*/
http.adornUrl = (actionName) =>
// 非生产环境 && 开启代理, 接口前缀统一使用[/proxyApi/]前缀做代理拦截!
return (process.env.OPEN_PROXY ? '/proxyApi' : window.SITE_CONFIG.baseUrl) + actionName
/**
* 请求方法处理
* @param methodName
* @returns *
*/
http.adornMethod = (methodName) =>
return methodName;
/**
* get请求参数处理
* @param * params 参数对象
* @param * openDefultParams 是否开启默认参数?
*/
http.adornParams = (params = , openDefultParams = true) =>
var defaults =
't': new Date().getTime()
return openDefultParams ? merge(defaults, params) : params
/**
* post请求数据处理
* @param * data 数据对象
* @param * openDefultdata 是否开启默认数据?
* @param * contentType 数据格式
* json: 'application/json; charset=utf-8'
* form: 'application/x-www-form-urlencoded; charset=utf-8'
*/
http.adornData = (data = , openDefultdata = true, contentType = 'json') =>
var defaults =
't': new Date().getTime()
data = openDefultdata ? merge(defaults, data) : data
return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
export default http
4.2.2、请求地址代理方式
config/index.js
const devEnv = require('./dev.env')
proxyTable: devEnv.OPEN_PROXY === false ? :
'/proxyApi':
target: 'http://127.0.0.1:8888/',
changeOrigin: true,
pathRewrite:
'^/proxyApi': '/'
,
4.2.3、请求地址常量方式
static/config/index.js
;(function ()
window.SITE_CONFIG = ;
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8888';
// cdn地址 = 域名 + 版本号
window.SITE_CONFIG['domain'] = './'; // 域名
window.SITE_CONFIG['version'] = ''; // 版本号(年月日时分)
window.SITE_CONFIG['cdnUrl'] = window.SITE_CONFIG.domain + window.SITE_CONFIG.version;
)();
Index.html引入index.js
<script src="./static/config/index.js"></script>
4.3、登录页面实现
使用统一请求工具类
this.$http(
url: this.$http.adornUrl("/sys/login"),
method: 'post',
params: this.$http.adornParams(this.dataForm)
).then((data) =>
if (data.success)
this.$router.replace( name: 'home' )
else
this.dataForm.password="";
this.$message.error(data.message)
this.loading = false;
).catch(data =>
this.loading = false;
this.dataForm.password="";
this.$message.error(this.tips.error);
)
4.4密码加密传输
当前密码为明文传输,很不安全,所以需要加密传输
4.4.1、前端加密
npm install jsencrypt –d
工具类
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\\n' +
'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
// 加密
export function encrypt(txt)
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
登录页面
import encrypt from '@/utils/jsencrypt'
this.dataForm.password=encrypt(this.dataForm.password)
4.4.2后端解密
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
添加工具类
package com.sq.auth.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
@Slf4j
public class RSAEncrypt
// 密钥对生成 http://web.chacuo.net/netrsakeypair
private static String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\\n" +
"nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==";
private static String privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\\n" +
"7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\\n" +
"PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\\n" +
"kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\\n" +
"cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\\n" +
"DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\\n" +
"YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\\n" +
"UP8iWi1Qw0Y=";
/**
* RSA公钥加密
* @param str 加密字符串
* @param publicKey 公钥
* @return 密文
* @throws Exception 加密过程中的异常信息
*/
public static String encrypt( String str, String publicKey ) throws Exception
//base64编码的公钥
byte[] decoded = Base64.decodeBase64(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
String outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes("UTF-8")));
return outStr;
/**
* RSA私钥解密
* @param str 加密字符串
* @return 铭文
* @throws Exception 解密过程中的异常信息
*/
public static String decrypt(String str) throws Exception
String outStr="";
try
//64位解码加密后的字符串
byte[] inputByte = Base64.decodeBase64(str.getBytes("UTF-8"));
//base64编码的私钥
byte[] decoded = Base64.decodeBase64(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
outStr = new String(cipher.doFinal(inputByte));
catch (Exception e)
log.error("RSA解密失败",e);
finally
return outStr;
4.5、登出实现
this.$http(
url: this.$http.adornUrl('/sys/logout'),
method: 'post',
data: this.$http.adornData()
).then((data) =>
if (data && data.success === true)
this.$router.push( name: 'login' )
).catch(() =>
this.$message.error(this.tips.error);
)
关注公众号”小猿架构“,发送 "前后分离架构" ,下载课程视频+课程源码+课件。
手把手教你前后分离架构 SpringBoot连接数据库
1、连接数据库
管理系统离不开关系数据库的支持, 数据库采用mysql数据库。
1.1、数据库创建
MySQL在5.5.3之后增加了utf8mb4的字符集,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。utf8mb4是utf8的超集,除了将编码改为utf8mb4外不需要做其他转换。 utf8mb4 是目前最大的一个字符编码,支持任意文字。
utf8mb4对应的排序字符集有utf8mb4_unicode_ci、utf8mb4_general_ci.
utf8mb4_unicode_ci是基于标准的Unicode来排序和比较,能够在各种语言之间精确排序.在特殊情况下,Unicode排序规则为了能够处理特殊字符的情况,实现了略微复杂的排序算法。
utf8mb4_general_ci没有实现Unicode排序规则,在遇到某些特殊语言或者字符集,排序结果可能不一致。
utf8mb4_unicode_ci 校对速度快,但准确度稍差。utf8_unicode_ci准确度高,但校对速度稍慢,两者都不区分大小写。通常情况下,新建数据库时一般选用 utf8mb4_general_ci 就可以了
//创建数据
CREATE DATABASE mir DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
//创建用户并授权
CREATE USER 'mir'@'%' IDENTIFIED BY '123456';
GRANT ALL PRIVILEGES ON mir.* TO 'mir'@'%';
FLUSH PRIVILEGES;
MySQL存储引擎
Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
MyIASM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
1.2、整合Mybaties-Plus
有很多持久层框架帮助我们更方便的操作数据库。常用的持久层框架有MyBatis 、hibernate等等。我们采用Mybatis-Plus作为系统持久层框架,Mybatis-Plus是一个Mybatis的增强工具,只是在Mybatis的基础上做了增强却不做改变,MyBatis-Plus支持所有Mybatis原生的特性,所以引入Mybatis-Plus不会对现有的Mybatis构架产生任何影响。Mybatis-Plus有依赖少、损耗小、预防Sql注入等诸多优点。
官网地址:安装 | MyBatis-Plus
1.2.2、依赖安装
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
1.2.2、添加配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/mir
spring.datasource.username=mir
spring.datasource.password=123456
1.2.3、添加config配置
@Configuration
@MapperScan("com.example.*.mapper")
public class MyBatisPlusConfig
如果配置全局扫描可以在Mapper接口添加 @Mapper 注解。
1.2.4、代码生成器
手动参照数据库字段编写对象实体等字段,很麻烦而且容易出错,MyBatis-Plus 代码生成器可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
https://baomidou.com/pages/779a6e/#快速入门 参照官网
mybatis-plus-generator 3.5.1 及其以上版本可使用新版本代码生成器。
添加依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
样例
public static void main(String[] args)
String url = "jdbc:mysql://localhost:3306/mir";
String username = "mir";
String password = "123456";
FastAutoGenerator.create(url, username, password)
.globalConfig(builder ->
builder.author("dehuisun") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D://generator"); // 指定输出目录
)
.packageConfig(builder ->
builder.parent("com.xgg") // 设置父包名
.moduleName("sys") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "D://generator")); // 设置mapperXml生成路径
)
.strategyConfig(builder ->
builder.addInclude("sys_user") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
)
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
1.3、数据持久化规范
为了更好研发建议迭代系统,所以我们需要制定一些研发规范来帮助我们快速迭代。
1.3.1、数据库建表规范
为数据库表添加一些公共字段,比如deleted(逻辑删除标识)、create_time、update_time、create_by、update_by等等来实现数据添加变更记录、逻辑删除、乐观锁等业务需求。帮助我们实现数据恢复及数据修改回溯。
1.3.2、自动填充公共字段
业务表中有create_time、update_time、create_by、update_by这四个字段,create_time、update_time要自动填充为当前时间,create_by、update_by自动填充为当前登录的用户ID。
配置类
@Bean
public OptimisticLockerInterceptor mybatisPlusInterceptor()
return new OptimisticLockerInterceptor();
@Bean
public MetaObjectHandler metaObjectHandler()
return new MetaObjectHandler()
@Override
public void insertFill(MetaObject metaObject)
SysUser user = getUserId(metaObject);
if (!Objects.isNull(user))
this.strictInsertFill(metaObject, "createBy", Long.class, user.getId());
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
@Override
public void updateFill(MetaObject metaObject)
SysUser user = getUserId(metaObject);
if (!Objects.isNull(user))
this.strictUpdateFill(metaObject, "updateBy", Long.class, user.getId());
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
private SysUser getUserId(MetaObject metaObject)
//自己认证框架获取登录用户的方法
;
1.3.3、逻辑删除
添加配置文件
mybatis-plus.global-config.db-config.logic-delete-field=deleted
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
实体注解
@TableLogic
private Integer deleted;
1.3.4抽象公共实体类
@Getter
public class BaseEntity
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private Long createUserId;
@TableField(fill = FieldFill.UPDATE)
private Long updateUserId;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
公共实体类只添加getter方法,防止手动set赋值字段。
继承公共实体:
还有乐观锁等特性,可以后续根据需求来实现。
2、用户管理功能实现
2.1、后端实现
2.1.1 分层规范
系统参照阿里分层规范
• Web 层:controller层主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
• Service 层:相对具体的业务逻辑服务层。
• Manager 层:通用业务处理层,它有如下特征:
1) 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。
2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
3) 与 DAO 层交互,对多个 DAO 的组合复用。
• DAO 层:数据访问Mapper层,与底层 MySQL、Oracle、Hbase、OB 等进行数据交互。
2.1.2、创建用户表
CREATE TABLE `sys_user` (
`id` BIGINT NOT NULL,
`account` VARCHAR(50) NOT NULL COMMENT '账号',
`username` VARCHAR(240) NOT NULL COMMENT '用户名',
`password` VARCHAR(100) NOT NULL COMMENT '密码',
`salt` VARCHAR(64) DEFAULT NULL COMMENT '盐',
`email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
`mobile` VARCHAR(64) DEFAULT NULL COMMENT '手机号',
`status` SMALLINT(6) DEFAULT '0' COMMENT '状态(0:正常1:停用2:锁定)',
`err_num` TINYINT DEFAULT NULL COMMENT '登录错误次数',
`lock_time` DATETIME DEFAULT NULL COMMENT '锁定时间',
`create_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '创建者ID',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`update_by` BIGINT UNSIGNED DEFAULT NULL COMMENT '修改人ID',
`update_time` DATETIME DEFAULT NULL COMMENT '修改时间',
`deleted` TINYINT DEFAULT '0' COMMENT '是否被删除(0:未删除,1:已删除)',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='用户管理'
2.1.3、代码生成
生成的代码拷贝至项目
2.1.4、接口实现
@RestController
@Api(tags = "用户管理服务")
@RequestMapping("/sys/user")
public class UserController
@Resource
private IUserService userService;
@GetMapping("/page")
@ApiOperation(value = "用户列表分页查询")
public Result<Page<UserVO>> getPageList(@Valid UserPageParam userParam)
Page<User> page = new Page(userParam.getCurrent(), userParam.getSize());
userService.pageList(page, userParam.getUsername());
Page<UserVO> pageResult = CollectionUtils.page(page, UserVO.class);
return Result.ok().info(pageResult);
@ApiOperation("用户查询")
@GetMapping("/id")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, paramType = "path", dataType = "Long")
public Result<UserVO> getInfo(@PathVariable("id") Long id)
User user = userService.getById(id);
UserVO userVO = new UserVO();
if(user!=null)
BeanUtils.copyProperties(user, userVO);
return Result.ok().info(userVO);
@ApiOperation("用户删除")
@DeleteMapping("/id")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, paramType = "path", dataType = "Long")
public Result delete(@PathVariable Long id) throws Exception
// User user = (User) SecurityUtils.getSubject().getPrincipal();
// if (sysUser.getId().equals(id))
// return Result.error().message("不能删除当前登录用户");
//
userService.removeById(id);
return Result.ok();
@ApiOperation("用户新增")
@PostMapping("")
public Result save(@Validated(AddGroup.class) @RequestBody UserParam userParam)
User sysUser = new User();
sysUser.setAccount(userParam.getAccount());
sysUser.setUsername(userParam.getUsername());
List<User> list = userService.list(new QueryWrapper<>(sysUser));
if (list!=null&&list.size() != 0)
return Result.error().message("该账号已存在!");
BeanUtils.copyProperties(userParam, sysUser);
sysUser.setId(null);
sysUser.setPassword(Constant.PASSWORD);
userService.save(sysUser);
return Result.ok();
@ApiOperation("用户更新")
@PutMapping("")
public Result update(@Validated(UpdateGroup.class) @RequestBody UserParam userParam)
User sysUser = new User();
sysUser.setAccount(userParam.getAccount());
sysUser.setUsername(userParam.getUsername());
List<User> list = userService.list(new QueryWrapper<>(sysUser));
if (list!=null&&list.size() != 0)
return Result.error().message("该账号已存在!");
sysUser = userService.getById(userParam.getId());
BeanUtils.copyProperties(userParam, sysUser);
userService.updateById(sysUser);
return Result.ok();
接口实现参数校验(分组校验等多种方式)、统一返回、每层实体参数规划。用户实体继承公共参数。
2.1.5 解决精度丢失问题
@Configuration
@EnableWebMvc
public class JacksonConfig implements WebMvcConfigurer
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters)
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
/**
* 序列换成json时,将所有的long变成string
* 因为js中得数字类型不能包含所有的java long值
*/
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
converters.add(jackson2HttpMessageConverter);
2.1.6参数序列化问题
@JsonIgnoreProperties(ignoreUnknown = true)
2.2、前端实现
2.2.1实现列表和详情页
<template>
<div>
<el-form :model="tableForm" ref="tableForm">
<el-row :gutter="20" >
<el-col :xs="24" :sm="6" >
<el-form-item prop="classCode">
<el-input v-model="tableForm.name" placeholder="姓名" maxlength="20" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="span:24,offset:0" :sm="span:18,offset:0">
<el-form-item style="float: right">
<el-button type="primary" @click="getTableData()" icon="el-icon-search">查询</el-button>
<el-button type="default" @click="resetForm('tableForm')" plain icon="el-icon-refresh-left">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-divider></el-divider>
<el-row style="padding-bottom: 5px;">
<el-col :span="24">
<el-button type="primary" icon="el-icon-plus" @click="doEdit()">新增</el-button>
<!-- <el-button type="primary" icon="el-icon-plus" @click="doDrawer()">抽屉</el-button>-->
</el-col>
</el-row>
<el-table
header-cell-class-name="custom_table_header"
:data="tableData.records"
border
v-loading="tableDataLoading"
style="width: 100%">
<el-table-column
type="index"
label="#"
align="center"
fixed="left"
width="50">
</el-table-column>
<el-table-column prop="account" header-align="center" align="center" label="登录账号"></el-table-column>
<el-table-column prop="username" header-align="center" align="center" label="用户名"></el-table-column>
<el-table-column prop="mobile" header-align="center" align="center" label="手机号"></el-table-column>
<el-table-column prop="email" header-align="center" align="center" label="邮箱"></el-table-column>
<el-table-column prop="status" header-align="center" align="center" label="状态">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 0" >正常</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" header-align="center" align="center" width="200"
label="创建时间" :formatter="formatterDateTime"></el-table-column>
<el-table-column fixed="right" header-align="center" align="center" width="120" label="操作">
<template slot-scope="scope">
<el-button type="text" @click="doEdit(scope.row.id)">编辑</el-button>
<el-button type="text" @click="doDel(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="tableData.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="tableData.size"
:total="tableData.total"
:layout="pageLayout">
</el-pagination>
<add-dialog ref="addDialog" @refreshDataList="getTableData"></add-dialog>
<add-drawer ref="addDrawer" @refreshDataList="getTableData"></add-drawer>
</div>
</template>
<script>
import AddDialog from './user-addDialog'
import AddDrawer from './user-addDrawer'
import axios from 'axios'
import formartDateTime from '@/utils'
export default
data()
return
tableForm:
name: '',
province: ''
,
tableData:
records: [],
total: 0,
size: 10,
current: 1,
pages: 1
,
tableDataLoading: false,
tableDataSelections: [],
,
components:
AddDialog,
AddDrawer
,
methods:
// 获取数据列表
getTableData()
this.tableForm.current = this.tableData.current;
this.tableForm.size = this.tableData.size;
this.tableDataLoading = true;
axios.get('http://127.0.0.1:8888/sys/user/page',
params: this.tableForm
).then((data) =>
if (data.success)
this.tableData = data.info;
this.tableDataLoading = false
else
this.$message.error(data.message)
).catch(() =>
this.$message.error(this.tips.error);
)
,
// 每页数
sizeChangeHandle(val)
this.tableData.size = val;
this.getTableData()
,
// 当前页
currentChangeHandle(val)
this.tableData.current = val;
this.getTableData()
,
handleClick(row)
console.log(row);
,
onSubmit()
console.log('submit!');
,
doEdit(id)
this.$nextTick(()=>
this.$refs.addDialog.init(id);
)
,
// 删除
doDel(id)
this.$confirm(this.tips.isSure, this.tips.tips, ).then(() =>
//防止表单重复提交
this.$MessageBox.showLoading();
axios.delete(`http://127.0.0.1:8888/sys/user/$id`
).then((data) =>
this.$MessageBox.hideLoading();
this.getTableData();
if (data.success)
this.$message.success(data.message)
else
this.$message.error(data.message)
).catch(() =>
this.$MessageBox.hideLoading()
this.$message.error(this.tips.error);
)
)
,
doDrawer(id)
this.$nextTick(()=>
this.$refs.addDrawer.init(id);
)
,
//时间格式化
formatterDateTime: function (row, column, cellValue, index)
return formartDateTime(cellValue)
,
,
activated()
this.getTableData()
,
computed:
pageLayout()
if (this.$store.state.common.clientType === 'phone') return 'total, sizes, prev, pager, next'
return 'total, sizes, prev, pager, next, jumper'
,
</script>
<style scoped>
</style>
<template>
<div>
<el-dialog title="" class="custom_dialog"
:close-on-click-modal = "false"
:visible.sync="visible">
<div slot="title" class="dialog-title">
<i class="el-icon-edit-outline"></i>
<span class="title-text">用户维护</span>
</div>
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="doSubmit()" :label-width="formLabelWidth">
<el-form-item label="账号" prop="account">
<el-input v-model="dataForm.account" autocomplete="off" ></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="dataForm.username" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="dataForm.mobile" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="dataForm.email" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="visible = false"><i class="el-icon-close"></i>取 消</el-button>
<el-button type="primary" @click="doSubmit()"><i class="el-icon-check"></i>确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
import isEmail, isMobile from '@/utils/validate'
export default
data()
var validateEmail = (rule, value, callback) =>
if (value && !isEmail(value))
callback(new Error('邮箱格式错误'))
else
callback()
var validateMobile = (rule, value, callback) =>
if (value && !isMobile(value))
callback(new Error('手机号格式错误'))
else
callback()
return
visible: false,
dataForm:
id: '',
account: '',
username: '',
email: '',
mobile: '',
status: ''
,
formLabelWidth: '120px',
dataRule:
account: [
required: true, message: '账号不能为空', trigger: 'blur',
min: 3, max: 5, message: '账号长度在 3 到 10 个字符', trigger: 'blur'
],
username: [
required: true, message: '用户名不能为空', trigger: 'blur',
min: 2, max: 5, message: '用户名长度在 2 到 5 个字符', trigger: 'blur'
],
email: [
required: true, message: '邮箱不能为空', trigger: 'blur' ,
validator: validateEmail, trigger: 'blur'
],
mobile: [
required: true, message: '手机号不能为空', trigger: 'blur' ,
validator: validateMobile, trigger: 'blur'
]
;
,
methods:
init(id)
this.visible = true;
this.dataForm.id = id || ''
this.$nextTick(() =>
this.$refs['dataForm'].resetFields()
)
if (this.dataForm.id)
axios.get(`http://127.0.0.1:8888/sys/user/$this.dataForm.id`,
).then((data) =>
if (data.success)
this.dataForm = data.info;
)
,
doSubmit()
this.$refs['dataForm'].validate((valid) =>
if (valid)
//防止表单重复提交
this.$MessageBox.showLoading()
if (this.dataForm.id)
axios.put('http://127.0.0.1:8888/sys/user', this.dataForm
).then((data) =>
this.$MessageBox.hideLoading()
if (data.success)
this.visible = false
this.$emit('refreshDataList')
this.$message.success(data.message)
else
this.$message.error(data.message)
).catch((data) =>
this.$MessageBox.hideLoading()
this.$message.error(this.tips.error);
)
else
axios.post('http://127.0.0.1:8888/sys/user', this.dataForm
).then((data) =>
this.$MessageBox.hideLoading()
this.visible = false
this.$emit('refreshDataList')
if (data.success)
this.$message.success(data.message)
else
this.$message.error(data.message)
).catch((data) =>
this.$MessageBox.hideLoading()
this.$message.error(this.tips.error);
)
)
,
;
</script>
<style scoped>
</style>
2.2.2添加路由
path: '/user', name: 'user', component: _import('sys/user-list'),meta: title:'用户管理',isTab:true,
2.2.3、添加菜单
<el-menu-item index="1-4" @click="$router.push( name: 'user' )">用户管理</el-menu-item>
2.2.4、添加常量类
tips.js
const error = "系统异常,请稍后重试"
export default
error
Main.js
import tips from "@/constants/tips.js"
Vue.prototype.tips = tips
2.2.5、测试
关注公众号”小猿架构“,发送 "前后分离架构" ,下载课程视频+课程源码+课件。
以上是关于手把手教你前后分离架构 系统认证鉴权实现的主要内容,如果未能解决你的问题,请参考以下文章