项目前端大部分都写完了, 现在补后端并和前端配合修改一部分代码。
后端环境搭建
- spring boot官网下载初始项目包;
- 配置项目编码等
- 数据库源设置好,并将相关配置写入配置文件
spring.datasource.url=jdbc:mysql:...
spring.datasource.username=....
spring.datasource.password=....
- 修改启动日志
- 修改logback日志格式(resources下添加logbackspring.xml)
- 增加启动成功日志; 启动类XXXXApplication修改为:
private static final Logger LOG = LoggerFactory.getLogger(XXXXApplication.class);
public static void main(String[] args) {
xxxxApplication app = new SpringApplication(XXXXXApplication.class);
Environment env = app.run(args).getEnvironment();
LOG.info(("启动成功!"));
LOG.info(("地址:\thttp://127.0.0.1:{}"),env.getProperty("server.port"));
}
- hello world接口测试;HTTP Client接口测试;
- 接口获取实际数据库信息测试
打算获取user表的信息,首先需要创建好controller、 service和entity以及repositry,
(1)自动生成entity
(2)repositry创建(启动过程中报错,原因是忘记添加@Entity注解),需要在前面的自动生成entity脚本中加入out.println "@Entity"
(3)service 、controller和test脚本完成,然后执行测试,报错下面的内容
javax.servlet.ServletException: Circular view path [login]: would dispatch back to the current handler URL [/login] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
原因是未加@RestController注解
- 解决跨域问题 后端
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//是否发送Cookie
.allowCredentials(true)
//设置放行哪些原始域 SpringBoot2.4.4下低版本使用.allowedOrigins("*")
.allowedOriginPatterns("*")
//放行哪些请求方式
.allowedMethods("GET", "POST", "PUT", "DELETE")
//.allowedMethods("*") //或者放行全部
//放行哪些原始请求头部信息
.allowedHeaders("*")
//暴露哪些原始请求头部信息
.exposedHeaders("*");
}
}
前端(vue官方文档有写)链接
注册功能
- 这部分我之前写的是用邮箱注册,这次决定改为手机号注册。毕竟手机号注册在国内用得更多。
- 测试注册功能时报错SQL Error: 1064, SQLState: 42000 原因: 数据库有一项名字是保留字use
- 测试时报错:"message": "Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported". 原因:测试写错
POST http://localhost:8081/api/v1/account/sms/send
Content-Type: application/json
{
"mobile": 123434,
"forWhat": "R"
}
- 数据前端验证
- 这块我之前把大部分内容已经写好了,但这次看竟然有点不记得了..尴尬。
重新研究了一下,我这块前端验证是这样的:
写了一个方法放入mixin中,通过验证把错误信息放入this.error里,然后直接在登录注册相关方法里调用validate这个方法。
3.其他
- 问题:手机号输入之后后端提示手机号为空: 原因是这块数据表column名中间改过,前端忘记改
- 问题:发送验证码后button的倒计时和禁用没达到预期效果。解决:代码有低级错误,变量写错。禁用方面在全局css为禁用的button设置鼠标效果。
- 增加后端发送短信验证码时先检查手机号是否已存在
- 增加前端验证码输入框
- 增加前端密码加密功能
token的生成和后端验证拦截
参考 Token Util
package com.star.string.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class TokenUtil {
/**
* token过期时间
*/
private static final long EXPIRE_TIME = 30L * 24 * 60 * 60 * 1000;
/**
* token秘钥
*/
private static final String TOKEN_SECRET = "BEST_SELF";
/**
* 生成签名,30天后过期
* @param mobile 手机号
* @param loginTime 登录时间
* @return 生成的token
*/
public static String sign(String mobile, String loginTime) {
try {
// 设置过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("mobile", mobile)
.withClaim("loginTime", loginTime)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检验token是否正确
* @param token 需要校验的token
* @return 校验是否成功
*/
public static boolean verify(String token){
try {
//设置签名的加密算法:HMAC256
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e){
return false;
}
}
}
Token Service
import com.star.string.util.TokenUtil;
import org.springframework.stereotype.Service;
import javax.xml.crypto.Data;
import java.util.Date;
@Service
public class TokenService {
public String generateToken(String mobile){
return TokenUtil.sign(mobile, String.valueOf(new Date()));
}
}
拦截器及其配置
import com.alibaba.fastjson2.JSONObject;
import com.star.string.util.TokenUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
response.setCharacterEncoding("utf-8");
String token = request.getHeader("Authorization");
if (token != null) {
boolean result = TokenUtil.verify(token);
if (result) {
System.out.println("通过拦截器");
return true;
}
}
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
JSONObject json = new JSONObject();
json.put("success", "false");
json.put("msg", "认证失败,未通过拦截器");
json.put("code", "500");
response.getWriter().append(json.toJSONString());
System.out.println("认证失败,未通过拦截器");
} catch (Exception e) {
e.printStackTrace();
response.sendError(500);
return false;
}
return false;
}
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
private final TokenInterceptor tokenInterceptor;
//构造方法
private InterceptorConfig interceptorConfig;
public InterceptorConfig(TokenInterceptor tokenInterceptor){
this.tokenInterceptor = tokenInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/user/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
- 前端拦截相关配置
在router中为需要拦截的页面设置meta: { requireAuth: true },然后
router.beforeEach((to, from, next) => {
// console.log(to.path)
// console.log(store.getters.isLogged)
let isAuthenticated = store.getters.isLogged
//如果访问的是需要权限的页面并且用户并未处于登录状态
if (to.matched.some(record => record.meta.requireAuth) && !isAuthenticated) {
//如果访问的是登录后用户主页,转向游客主页,否则转向登录页面
if (to.path === '/home') {
next({ name: 'landingPage' })
} else {
next({ path: '/login' })
}
} else{
next()
}
退出登录
因为登录时把用户信息和token存在了local storage里,所以退出登录时要进行信息的清理。
登录时把信息存入
if(res.success){
const token = res.message
const loginUser = res.content
this.$store.dispatch('login',{loginUser, token})
....
}
store部分
const vuexLocal = new VuexPersistence({
})
export default createStore({
state() {
return {
loginUser: null,
token: null,
}
},
getters: {
isLogged(state) {
return state.loginUser !== null && state.token !== null
},
user(state) {
return state.loginUser
},
token(state){
return state.token
},
},
mutations: {
login(state, { loginUser, token }) {
state.loginUser = loginUser
state.token = token
},
update(state,user){
state.loginUser = user
window.localStorage.clear()
},
clear(state){
state.loginUser = null
state.token = null
}
},
actions: {
login(context,{loginUser,token}){
context.commit('login',{loginUser,token})
},
logout(context){
localStorage.clear()
context.commit('clear')
}
},
plugins: [
vuexLocal.plugin
]
})
退出登录时清除信息并转回landingpage
const res = await AccountService.logout(this.token)
if(res.success){
this.$router.push('/')
this.$store.dispatch('logout')
同时后端将token数据库对应条目设置为已失效
登录
- 1. 给登录前端加上密码加密
- 2. 已登录后进入landingpage和login/register/找回密码页面时自动跳转到登录后首页
方法: 在router中给已登录用户不可访问的页面加上meta属性
meta:{LoggedUserVisitable : false}
然后在router.beforeEach方法中添加下面内容
if(to.matched.some(record => record.meta.LoggedUserVisitable === false) && isAuthenticated){
next({path: '/home'})
- 3. 根据前端是否勾选保持30天登录修改后端token的有效期(改为24小时)。先用1分钟有效期来进行测试。(之后做)
- 4. 将退出登录后跳转的页面改为landingpage(存疑,目前退出后自动就跳转到了登录页面)