前言
没想到又鸽了一天,okey,调整心情,我们接着出发。那么今天要实现的就是我们第一个基本业务的实现,那就是登录注册功能。这个功能做好了就意味着前后端基本打通,接下来的工作就是划水就好了。当然对于接下来的计划是,接下来可能会来个Python研发系列的文章。说来惭愧哈,我是后面十二月分决定放弃考研,去准备就业然后海投的。到后面这个今年的秋招大环境懂得都懂,这个时候基本上没见着啥漏,Java方向没有找到合适的工作(我的要求不高,我两个兄弟秋招1.2W,我的目标也是这个)可惜的是现在放出的Java岗位要么是社招,要么校招钱太少。最后也非常感谢大佬给机会,Python方向找到一个还不错的实习。所以先干干Python呗,那么至于为什么毕设要用Java,很简单,这个架构,我们在那个开题答辩的时候就确定了,所以改的话要申请,说明啥的,比较麻烦,当然当初也想的是Java能直接找到个合适的工作。谁知道呢,无语了,那么同样的,python的另一个小伙伴golang也会重新搞一波。
okey,废话不多说,我们来看看我们要实现的效果
首先我们来看到前端: 首先未登录的情况下显示这个: 然后的话进入登录注册页面
之后登录之后,我们会回到主页 然后就会看到头像: 这个头像看起来有点眼熟哈。
前端
毫无疑问,我们首先还是先来看到我们的前端,这次的话,我会尽可能写详细一点儿。 当然这里的话,我们使用的是vue3
页面设计
首先我们来看到我们前端的设计。 我们整个html部分长这样:
java
<template>
<div class="main">
<div ref="container" class="container" id="container">
<div class="form-container sign-up-container">
<form action="#">
<h3>创建账户</h3>
<div class="social-container">
<a href="#" class="social"><i class="fab fa-facebook-f"></i></a>
<a href="#" class="social"><i class="fab fa-google-plus-g"></i></a>
<a href="#" class="social"><i class="fab fa-linkedin-in"></i></a>
</div>
<span>注册账户(暂不支持第三方)</span>
<input v-model="registQ.username" type="text" placeholder="账号(长度为8-12位)" />
<input v-model="registQ.nickname" type="text" placeholder="昵称(长度为6-12位)" />
<input v-model="registQ.password" type="password" placeholder="密码(长度为6-12位)" />
<input v-model="registQ.email" type="email" placeholder="邮箱(必须填写)" />
<input v-model="registQ.emailCode" type="text" placeholder="验证码(十分钟有效)" />
<div class="registerButton">
<el-button @click="getRegister">注册</el-button>
<el-button @click="getCode">{{ showSendButton }}</el-button>
</div>
</form>
</div>
<div class="form-container sign-in-container">
<form action="#">
<h3>登录</h3>
<div class="social-container">
<a href="#" class="social"><i class="fab fa-facebook-f"></i></a>
<a href="#" class="social"><i class="fab fa-google-plus-g"></i></a>
<a href="#" class="social"><i class="fab fa-linkedin-in"></i></a>
</div>
<span>使用账户登录(暂不支持第三方)</span>
<input v-model="loginQ.userName" type="text" placeholder="账号" />
<input v-model="loginQ.passWord" type="password" placeholder="密码" />
<a class="forget" href="#">忘记密码? >></a>
<el-button @click="LoginGo">登录</el-button>
</form>
</div>
<div class="overlay-container">
<div class="overlay">
<div class="overlay-panel overlay-left">
<h1>欢迎登录!</h1>
<p>登录后,30天自动登录</p>
<button @click="signInSwitch" class="ghost" id="signIn"> 登录</button>
</div>
<div class="overlay-panel overlay-right">
<h1>Hello, Friend!</h1>
<p>创建账户,加入我们</p>
<button @click="signUpSwitch" class="ghost" id="signUp">注册</button>
</div>
</div>
</div>
</div>
<Vcode :show="isShow" @success="success" @close="close" @fail="fail"></Vcode>
</div>
</template>
然后我们整个页面对应的css代码是这样的:
css
.registerButton {
display: flex;
gap: 5rem;
}
.forget:hover {
color: #04a5ea;
}
.main {
width: 90%;
margin: 0 auto;
height: 33.125rem;
}
* {
box-sizing: border-box;
}
body {
background: #f6f5f7;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: 'Montserrat', sans-serif;
height: 100vh;
margin: -1.25rem 0 3.125rem;
}
h1 {
font-weight: bold;
margin: 0;
}
h2 {
text-align: center;
}
p {
font-size: 0.875rem;
font-weight: 100;
line-height: 1.25rem;
letter-spacing: 0.0313rem;
margin: 1.25rem 0 1.875rem;
}
span {
font-size: 0.75rem;
}
a {
color: #333;
font-size: 0.875rem;
text-decoration: none;
margin: 0.9375rem 0;
}
button {
border-radius: 1.25rem;
border: 0.0625rem solid #2b80ff;
background-color: #2b8eff;
color: #FFFFFF;
font-size: 0.75rem;
font-weight: bold;
padding: 0.75rem 1.5rem;
letter-spacing: 0.0625rem;
text-transform: uppercase;
transition: transform 80ms ease-in;
}
button:active {
transform: scale(0.95);
}
button:focus {
outline: none;
}
button.ghost {
background-color: transparent;
border-color: #FFFFFF;
}
form {
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 3.125rem;
height: 100%;
text-align: center;
}
input {
background-color: #eee;
border: none;
padding: 0.75rem 0.9375rem;
margin: 0.2rem 0;
width: 100%;
}
.container {
background-color: #fff;
border-radius: 0.625rem;
box-shadow: 0 0.875rem 1.75rem rgba(0, 0, 0, 0.25),
0 0.625rem 0.625rem rgba(0, 0, 0, 0.22);
position: relative;
overflow: hidden;
width: 48rem;
max-width: 100%;
min-height: 30rem;
margin: 3.125rem auto;
}
.form-container {
position: absolute;
top: 0;
height: 100%;
transition: all 0.6s ease-in-out;
}
.sign-in-container {
left: 0;
width: 50%;
z-index: 2;
}
.container.right-panel-active .sign-in-container {
transform: translateX(100%);
}
.sign-up-container {
left: 0;
width: 50%;
opacity: 0;
z-index: 1;
}
.container.right-panel-active .sign-up-container {
transform: translateX(100%);
opacity: 1;
z-index: 5;
animation: show 0.6s;
}
@keyframes show {
0%,
49.99% {
opacity: 0;
z-index: 1;
}
50%,
100% {
opacity: 1;
z-index: 5;
}
}
.overlay-container {
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 100%;
overflow: hidden;
transition: transform 0.6s ease-in-out;
z-index: 100;
}
.container.right-panel-active .overlay-container {
transform: translateX(-100%);
}
.overlay {
background: #04a5ea;
background: -webkit-linear-gradient(to right, #2b95ff, #4184ff);
background: linear-gradient(to right, #2bd5ff, #41adff);
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
color: #FFFFFF;
position: relative;
left: -100%;
height: 100%;
width: 200%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
.container.right-panel-active .overlay {
transform: translateX(50%);
}
.overlay-panel {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 2.5rem;
text-align: center;
top: 0;
height: 100%;
width: 50%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
.overlay-left {
transform: translateX(-20%);
}
.container.right-panel-active .overlay-left {
transform: translateX(0);
}
.overlay-right {
right: 0;
transform: translateX(0);
}
.container.right-panel-active .overlay-right {
transform: translateX(20%);
}
.social-container {
margin: 0 0;
}
.social-container a {
border: 0.0625rem solid #DDDDDD;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
margin: 0 0.3125rem;
height: 2.5rem;
width: 2.5rem;
}
那么同样的,为了实现一些动画效果,这里也是用了一点点的js.不过原理很简单,其实就是先写好了css的样式,动画效果,然后js,切换对应的class,就可以了。 那么在这里用到的就是这两个对象:
js
//切换选项卡
const container = ref();
const signUpSwitch = () => {
container.value.classList.add("right-panel-active");
}
const signInSwitch = () => {
container.value.classList.remove("right-panel-active");
}
这样一来,我们的整个页面都设计好了。
基本逻辑
聊完了整个页面上的设计,我们再来看到我们前端的基本逻辑。
axios封装
首先,我们这边第一件事情就是对axios进行封装,当然这里的话,只是进行了简单封装。
js
import axios from 'axios'
import StateStorage from '../utils/store'
import { defaultUserLogin,UserLoginR} from "./rentity"
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
// 设置基础的url配置项,这样接口处的url前面就不用写url:'http://127.0.0.1:8000/api/home',直接写成 url:'/api/home', 就可以了
baseURL: 'http://127.0.0.1:88/api/hlang-server',
//设置请求超时时间
timeout: 5000
})
// 2.axios的拦截器,用不到的可以忽略这节
// 2.1.请求拦截的作用
instance.interceptors.request.use(config => {
const userLoginR = StateStorage.getByLocalStateDefault("userLoginR") as UserLoginR
config.headers['loginType'] = 'PcType'
config.headers['userid'] = userLoginR.userid
config.headers['loginToke'] = userLoginR.token
return config
}, err => {
console.log('请求拦截err: '+err);
})
// 2.2.响应拦截
instance.interceptors.response.use(res => {
return res.data
}, err => {
console.log('响应拦截err: '+err);
})
// 3.发送真正的网络请求
return instance(config)
}
同时,由于我们这块使用的是ts,所以的话,我们这块还定义了几个基本的请求类。
请求类
主要是方便,前端和后端进行交互。
api封装
那么同样的对api进行简单封装。 之后的话,方便我们进行使用。
全局用户信息
之后的话,在定义一个全局的响应式的一个用户状态信息。
js
import { ref } from 'vue'
//用户登录之后的返回信息类
export interface UserLoginR {
token: String;
userid: String;
nickname: String;
upic: String;
login: boolean
}
//初始默认值,这是不能存在的值在系统当中,响应式的
export const defaultUserLogin= ref<UserLoginR>( {
token: "-1",
userid: "-1",
nickname: "-1",
upic: "-1",
login: false
});
状态存储
之后的话,就要聊到状态存储了。这里的话,就不用pinia,好吧主要是一直有个小bug不好解决。受不了,直接自己写一个工具类就得了。
js
/**
* 实现存储功能,这里不用pinia,插件有毒
*/
class StateStorage {
public static getBySession(key: string): object | undefined {
const json = sessionStorage.getItem(key);
if (json) {
return JSON.parse(json);
}
return undefined;
}
/**
* @param key
* @param value
*/
public static setBySession(key: string, value: object) {
const json = JSON.stringify(value);
sessionStorage.setItem(key, json);
}
/**
* @param key
* @returns
*/
public static getByLocalState(key: string): object | undefined {
const json = localStorage.getItem(key);
if (json) {
return JSON.parse(json);
}
return undefined;
}
/**
*
* @param key
* @param value
* @param time 单位是毫秒,不给默认是永久存储
*/
public static setByLocalState(key: string, value: object, time?: number) {
const json = JSON.stringify(value);
localStorage.setItem(key, json);
if (time) {
const expireTime = new Date().getTime() + time;
localStorage.setItem(`${key}_expiresIn`, expireTime.toString());
}
}
/**
* 默认存储30天
* @param key
* @param value
* @param days
*/
public static setByLocalStateDefault(
key: string,
value: object,
days: number = 30
) {
const json = JSON.stringify(value);
localStorage.setItem(key, json);
const expireTime = new Date().getTime() + days * 24 * 60 * 60 * 1000;
localStorage.setItem(`${key}_expiresIn`, expireTime.toString());
}
/**
* 获取默认存储30天的key
* @param key
* @returns
*/
public static getByLocalStateDefault(key: string): object | undefined {
const json = localStorage.getItem(key);
const expiresIn = localStorage.getItem(`${key}_expiresIn`);
if (json && expiresIn) {
const currentTime = new Date().getTime();
const expireTime = parseInt(expiresIn, 10);
if (currentTime <= expireTime) {
return JSON.parse(json);
} else {
localStorage.removeItem(key);
localStorage.removeItem(`${key}_expiresIn`);
}
}
return undefined;
}
}
export default StateStorage
注册逻辑
okey, 这里聊完了,我们来看到我们基本的注册逻辑。 其实很简单,前端的逻辑:
- 点击发送验证码,先在前端进行验证,例如前端的图形验证码
- 前端验证通过后,后端发一个邮箱验证码,然后这边输入验证码
- 携带账号,密码,验证码等信息完成注册
前端验证
我们先来看到前端的验证,这个前端的验证的话,直接使用第三方组件库。
html
npm install vue3-puzzle-vcode --save
基本用法的话,在这里的代码都有体现哈。
发送验证码
那么前端验证通过之后,我们发送验证码
js
//导数计时
const countdown = (seconds) => {
const intervalId = setInterval(() => {
seconds--;
//更新读秒
showSendButton.value = seconds + "s"
if (seconds < 0) {
showSendButton.value = "获取验证码"
isSendEmail.value = false
clearInterval(intervalId);
}
}, 1000);
}
// 获取验证码,这里先负责前端验证
const getCode = () => {
if (isSendEmail.value) {
ElMessage({
message: '请稍后再发送验证码',
type: 'warning',
})
return;
}
if (
StringUtil.checkLength(registQ.value.password, 6, 12) &&
StringUtil.checkLength(registQ.value.username, 8, 12) &&
StringUtil.isEmail(registQ.value.email) &&
StringUtil.checkLength(registQ.value.nickname, 6, 12)
) {
isShow.value = true
} else {
ElMessage({
message: '请按照要求填写账号,密码,邮箱,昵称',
type: 'warning',
})
}
}
const close = () => {
isShow.value = false
}
//前端验证通过,向后台请求验证码
const success = (msg) => {
isShow.value = false
//开始发送验证码
isSendEmail.value = true
//发送验证码
emailQ.value.password = registQ.value.password;
emailQ.value.username = registQ.value.username;
emailQ.value.email = registQ.value.email;
EmailCode(emailQ.value).then(
(res: any) => {
//服务器默认返回成功的状态码是0
if (res.code != 0) {
ElMessage({
message: res.msg,
type: 'error',
})
} else {
ElMessage({
message: "验证码发送成功",
type: 'success',
})
}
}
).catch(
err => {
console.log(err)
}
);
//倒计时
countdown(60)
}
这块的话,有个倒计时60秒
注册
然后注册就完了。
js
//正式注册
const getRegister = () => {
//还没有发送验证码
if (!isSendEmail.value) {
ElMessage({
message: '请先获取验证码',
type: 'warning',
})
return;
}
if (
StringUtil.checkLength(registQ.value.password, 6, 12) &&
StringUtil.checkLength(registQ.value.username, 8, 12) &&
StringUtil.isEmail(registQ.value.email) &&
StringUtil.checkLength(registQ.value.nickname, 6, 12) &&
StringUtil.checkLength(registQ.value.emailCode, 6, 10)
) {
//开始请求完成注册
Register(registQ.value).then(
(res: any) => {
if (res.code != 0) {
ElMessage({
message: res.msg,
type: 'error',
})
} else {
//切换到登录卡片
signInSwitch();
}
}
).catch(
err => {
console.log(err)
}
);
} else {
ElMessage({
message: '请按照要求填写账号,密码,邮箱,昵称,验证码',
type: 'warning',
})
}
}
登录逻辑
之后的话就是登录的逻辑,这个的话,拿着账号密码就好了。
js
//开始正式进行登录
const LoginGo = () => {
if (
StringUtil.checkLength(loginQ.value.passWord, 6, 12) &&
StringUtil.checkLength(loginQ.value.userName, 8, 12)
) {
Login(loginQ.value).then(
(res: any) => {
if (res.code != 0) {
ElMessage({
message: res.msg,
type: 'error',
})
} else {
//保存状态
defaultUserLogin.value = res.userLoginR
StateStorage.setByLocalStateDefault("userLoginR",defaultUserLogin.value);
//进入首页
router.push(
{
path: '/'
}
);
}
}
).catch(
err => {
console.log(err)
}
);
} else {
ElMessage({
message: '输入的账号,密码不合规',
type: 'warning',
})
}
}
前端完整代码
okey,那么这里再贴出前端代码参考一下,
html
<template>
<div class="main">
<div ref="container" class="container" id="container">
<div class="form-container sign-up-container">
<form action="#">
<h3>创建账户</h3>
<div class="social-container">
<a href="#" class="social"><i class="fab fa-facebook-f"></i></a>
<a href="#" class="social"><i class="fab fa-google-plus-g"></i></a>
<a href="#" class="social"><i class="fab fa-linkedin-in"></i></a>
</div>
<span>注册账户(暂不支持第三方)</span>
<input v-model="registQ.username" type="text" placeholder="账号(长度为8-12位)" />
<input v-model="registQ.nickname" type="text" placeholder="昵称(长度为6-12位)" />
<input v-model="registQ.password" type="password" placeholder="密码(长度为6-12位)" />
<input v-model="registQ.email" type="email" placeholder="邮箱(必须填写)" />
<input v-model="registQ.emailCode" type="text" placeholder="验证码(十分钟有效)" />
<div class="registerButton">
<el-button @click="getRegister">注册</el-button>
<el-button @click="getCode">{{ showSendButton }}</el-button>
</div>
</form>
</div>
<div class="form-container sign-in-container">
<form action="#">
<h3>登录</h3>
<div class="social-container">
<a href="#" class="social"><i class="fab fa-facebook-f"></i></a>
<a href="#" class="social"><i class="fab fa-google-plus-g"></i></a>
<a href="#" class="social"><i class="fab fa-linkedin-in"></i></a>
</div>
<span>使用账户登录(暂不支持第三方)</span>
<input v-model="loginQ.userName" type="text" placeholder="账号" />
<input v-model="loginQ.passWord" type="password" placeholder="密码" />
<a class="forget" href="#">忘记密码? >></a>
<el-button @click="LoginGo">登录</el-button>
</form>
</div>
<div class="overlay-container">
<div class="overlay">
<div class="overlay-panel overlay-left">
<h1>欢迎登录!</h1>
<p>登录后,30天自动登录</p>
<button @click="signInSwitch" class="ghost" id="signIn"> 登录</button>
</div>
<div class="overlay-panel overlay-right">
<h1>Hello, Friend!</h1>
<p>创建账户,加入我们</p>
<button @click="signUpSwitch" class="ghost" id="signUp">注册</button>
</div>
</div>
</div>
</div>
<Vcode :show="isShow" @success="success" @close="close" @fail="fail"></Vcode>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Vcode from 'vue3-puzzle-vcode'
import { useRouter } from 'vue-router'
import { RegisterQ, EmailQ, LoginQ } from '../api/qentity'
import { ElMessage } from 'element-plus'
import StringUtil from "../utils/StringUtil.js"
import { Register, EmailCode, Login } from "../api/api"
import { defaultUserLogin,UserLoginR} from "../api/rentity"
import StateStorage from '../utils/store'
const router = useRouter()
//是否显示验证码组件
const isShow = ref(false)
//是否正在发送验证码
const isSendEmail = ref(false)
const showSendButton = ref<String>("获取验证码")
//注册请求类
const registQ = ref<RegisterQ>({
email: "",
emailCode: "",
password: "",
username: "",
nickname: "",
})
//获取邮箱验证码的请求类
const emailQ = ref<EmailQ>({
password: "",
username: "",
email: ""
})
//登录请求类
const loginQ = ref<LoginQ>({
passWord: "",
userName: "",
type: "PcType"
})
//导数计时
const countdown = (seconds) => {
const intervalId = setInterval(() => {
seconds--;
//更新读秒
showSendButton.value = seconds + "s"
if (seconds < 0) {
showSendButton.value = "获取验证码"
isSendEmail.value = false
clearInterval(intervalId);
}
}, 1000);
}
// 获取验证码,这里先负责前端验证
const getCode = () => {
if (isSendEmail.value) {
ElMessage({
message: '请稍后再发送验证码',
type: 'warning',
})
return;
}
if (
StringUtil.checkLength(registQ.value.password, 6, 12) &&
StringUtil.checkLength(registQ.value.username, 8, 12) &&
StringUtil.isEmail(registQ.value.email) &&
StringUtil.checkLength(registQ.value.nickname, 6, 12)
) {
isShow.value = true
} else {
ElMessage({
message: '请按照要求填写账号,密码,邮箱,昵称',
type: 'warning',
})
}
}
const close = () => {
isShow.value = false
}
//前端验证通过,向后台请求验证码
const success = (msg) => {
isShow.value = false
//开始发送验证码
isSendEmail.value = true
//发送验证码
emailQ.value.password = registQ.value.password;
emailQ.value.username = registQ.value.username;
emailQ.value.email = registQ.value.email;
EmailCode(emailQ.value).then(
(res: any) => {
//服务器默认返回成功的状态码是0
if (res.code != 0) {
ElMessage({
message: res.msg,
type: 'error',
})
} else {
ElMessage({
message: "验证码发送成功",
type: 'success',
})
}
}
).catch(
err => {
console.log(err)
}
);
//倒计时
countdown(60)
}
//正式注册
const getRegister = () => {
//还没有发送验证码
if (!isSendEmail.value) {
ElMessage({
message: '请先获取验证码',
type: 'warning',
})
return;
}
if (
StringUtil.checkLength(registQ.value.password, 6, 12) &&
StringUtil.checkLength(registQ.value.username, 8, 12) &&
StringUtil.isEmail(registQ.value.email) &&
StringUtil.checkLength(registQ.value.nickname, 6, 12) &&
StringUtil.checkLength(registQ.value.emailCode, 6, 10)
) {
//开始请求完成注册
Register(registQ.value).then(
(res: any) => {
if (res.code != 0) {
ElMessage({
message: res.msg,
type: 'error',
})
} else {
//切换到登录卡片
signInSwitch();
}
}
).catch(
err => {
console.log(err)
}
);
} else {
ElMessage({
message: '请按照要求填写账号,密码,邮箱,昵称,验证码',
type: 'warning',
})
}
}
const fail = () => {
console.log('验证失败')
}
//切换选项卡
const container = ref();
const signUpSwitch = () => {
container.value.classList.add("right-panel-active");
}
const signInSwitch = () => {
container.value.classList.remove("right-panel-active");
}
//开始正式进行登录
const LoginGo = () => {
if (
StringUtil.checkLength(loginQ.value.passWord, 6, 12) &&
StringUtil.checkLength(loginQ.value.userName, 8, 12)
) {
Login(loginQ.value).then(
(res: any) => {
if (res.code != 0) {
ElMessage({
message: res.msg,
type: 'error',
})
} else {
//保存状态
defaultUserLogin.value = res.userLoginR
StateStorage.setByLocalStateDefault("userLoginR",defaultUserLogin.value);
//进入首页
router.push(
{
path: '/'
}
);
}
}
).catch(
err => {
console.log(err)
}
);
} else {
ElMessage({
message: '输入的账号,密码不合规',
type: 'warning',
})
}
}
</script>
<style scoped>
.registerButton {
display: flex;
gap: 5rem;
}
.forget:hover {
color: #04a5ea;
}
.main {
width: 90%;
margin: 0 auto;
height: 33.125rem;
}
* {
box-sizing: border-box;
}
body {
background: #f6f5f7;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: 'Montserrat', sans-serif;
height: 100vh;
margin: -1.25rem 0 3.125rem;
}
h1 {
font-weight: bold;
margin: 0;
}
h2 {
text-align: center;
}
p {
font-size: 0.875rem;
font-weight: 100;
line-height: 1.25rem;
letter-spacing: 0.0313rem;
margin: 1.25rem 0 1.875rem;
}
span {
font-size: 0.75rem;
}
a {
color: #333;
font-size: 0.875rem;
text-decoration: none;
margin: 0.9375rem 0;
}
button {
border-radius: 1.25rem;
border: 0.0625rem solid #2b80ff;
background-color: #2b8eff;
color: #FFFFFF;
font-size: 0.75rem;
font-weight: bold;
padding: 0.75rem 1.5rem;
letter-spacing: 0.0625rem;
text-transform: uppercase;
transition: transform 80ms ease-in;
}
button:active {
transform: scale(0.95);
}
button:focus {
outline: none;
}
button.ghost {
background-color: transparent;
border-color: #FFFFFF;
}
form {
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 3.125rem;
height: 100%;
text-align: center;
}
input {
background-color: #eee;
border: none;
padding: 0.75rem 0.9375rem;
margin: 0.2rem 0;
width: 100%;
}
.container {
background-color: #fff;
border-radius: 0.625rem;
box-shadow: 0 0.875rem 1.75rem rgba(0, 0, 0, 0.25),
0 0.625rem 0.625rem rgba(0, 0, 0, 0.22);
position: relative;
overflow: hidden;
width: 48rem;
max-width: 100%;
min-height: 30rem;
margin: 3.125rem auto;
}
.form-container {
position: absolute;
top: 0;
height: 100%;
transition: all 0.6s ease-in-out;
}
.sign-in-container {
left: 0;
width: 50%;
z-index: 2;
}
.container.right-panel-active .sign-in-container {
transform: translateX(100%);
}
.sign-up-container {
left: 0;
width: 50%;
opacity: 0;
z-index: 1;
}
.container.right-panel-active .sign-up-container {
transform: translateX(100%);
opacity: 1;
z-index: 5;
animation: show 0.6s;
}
@keyframes show {
0%,
49.99% {
opacity: 0;
z-index: 1;
}
50%,
100% {
opacity: 1;
z-index: 5;
}
}
.overlay-container {
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 100%;
overflow: hidden;
transition: transform 0.6s ease-in-out;
z-index: 100;
}
.container.right-panel-active .overlay-container {
transform: translateX(-100%);
}
.overlay {
background: #04a5ea;
background: -webkit-linear-gradient(to right, #2b95ff, #4184ff);
background: linear-gradient(to right, #2bd5ff, #41adff);
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
color: #FFFFFF;
position: relative;
left: -100%;
height: 100%;
width: 200%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
.container.right-panel-active .overlay {
transform: translateX(50%);
}
.overlay-panel {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 2.5rem;
text-align: center;
top: 0;
height: 100%;
width: 50%;
transform: translateX(0);
transition: transform 0.6s ease-in-out;
}
.overlay-left {
transform: translateX(-20%);
}
.container.right-panel-active .overlay-left {
transform: translateX(0);
}
.overlay-right {
right: 0;
transform: translateX(0);
}
.container.right-panel-active .overlay-right {
transform: translateX(20%);
}
.social-container {
margin: 0 0;
}
.social-container a {
border: 0.0625rem solid #DDDDDD;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
margin: 0 0.3125rem;
height: 2.5rem;
width: 2.5rem;
}
</style>
后端
okey, 终于到了俺们后端的模块了。 那么这里的话,我先来说一下,我们接下来会使用到的一些工具类。
工具类
邮箱工具类
java
@Service
public class MaliServiceImpl implements MailService {
/**
* 邮箱服务类
*/
@Autowired
private JavaMailSenderImpl javaMailSender;
@Value("${spring.mail.username}")
private String sendMailer;
/**
* 检测邮件信息类
* @param to
* @param subject
* @param text
*/
private void checkMail(String to,String subject,String text){
if(StringUtils.isEmpty(to)){
throw new RuntimeException("邮件收信人不能为空");
}
if(StringUtils.isEmpty(subject)){
throw new RuntimeException("邮件主题不能为空");
}
if(StringUtils.isEmpty(text)){
throw new RuntimeException("邮件内容不能为空");
}
}
/**
* 发送纯文本邮件
* @param to
* @param subject
* @param text
*/
@Override
public void sendTextMailMessage(String to,String subject,String text){
try {
//true 代表支持复杂的类型
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(),true);
//邮件发信人
mimeMessageHelper.setFrom(sendMailer);
//邮件收信人 1或多个
mimeMessageHelper.setTo(to.split(","));
//邮件主题
mimeMessageHelper.setSubject(subject);
//邮件内容
mimeMessageHelper.setText(text);
//邮件发送时间
mimeMessageHelper.setSentDate(new Date());
//发送邮件
javaMailSender.send(mimeMessageHelper.getMimeMessage());
System.out.println("发送邮件成功:"+sendMailer+"->"+to);
} catch (MessagingException e) {
e.printStackTrace();
System.out.println("发送邮件失败:"+e.getMessage());
}
}
/**
* 发送html邮件
* @param to
* @param subject
* @param content
*/
@Override
public void sendHtmlMailMessage(String to,String subject,String content){
content="<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
"<meta charset=\"utf-8\">\n" +
"<title>邮件</title>\n" +
"</head>\n" +
"<body>\n" +
"\t<h3>这是一封HTML邮件!</h3>\n" +
"</body>\n" +
"</html>";
try {
//true 代表支持复杂的类型
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(),true);
//邮件发信人
mimeMessageHelper.setFrom(sendMailer);
//邮件收信人 1或多个
mimeMessageHelper.setTo(to.split(","));
//邮件主题
mimeMessageHelper.setSubject(subject);
//邮件内容 true 代表支持html
mimeMessageHelper.setText(content,true);
//邮件发送时间
mimeMessageHelper.setSentDate(new Date());
//发送邮件
javaMailSender.send(mimeMessageHelper.getMimeMessage());
System.out.println("发送邮件成功:"+sendMailer+"->"+to);
} catch (MessagingException e) {
e.printStackTrace();
System.out.println("发送邮件失败:"+e.getMessage());
}
}
/**
* 发送带附件的邮件
* @param to 邮件收信人
* @param subject 邮件主题
* @param content 邮件内容
* @param filePath 附件路径
*/
@Override
public void sendAttachmentMailMessage(String to,String subject,String content,String filePath){
try {
//true 代表支持复杂的类型
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(javaMailSender.createMimeMessage(),true);
//邮件发信人
mimeMessageHelper.setFrom(sendMailer);
//邮件收信人 1或多个
mimeMessageHelper.setTo(to.split(","));
//邮件主题
mimeMessageHelper.setSubject(subject);
//邮件内容 true 代表支持html
mimeMessageHelper.setText(content,true);
//邮件发送时间
mimeMessageHelper.setSentDate(new Date());
//添加邮件附件
FileSystemResource file = new FileSystemResource(new File(filePath));
String fileName = file.getFilename();
mimeMessageHelper.addAttachment(fileName, file);
//发送邮件
javaMailSender.send(mimeMessageHelper.getMimeMessage());
System.out.println("发送邮件成功:"+sendMailer+"->"+to);
} catch (MessagingException e) {
e.printStackTrace();
System.out.println("发送邮件失败:"+e.getMessage());
}
}
/**
* 发送邮箱验证码
* @param to
* @param code
*/
@Override
public void sendCodeMailMessage(String to, String code) {
String subject = "Hlang社区邮箱验证码";
String text = "验证码10分钟内有效:"+code;
sendTextMailMessage(to,subject,text);
}
}
Redis工具类
java
public class RedisUtils {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public void expire(String key, long time) {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
} else {
throw new RuntimeException("超时时间小于0");
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @param tiemtype 时间类型
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key,TimeUnit tiemtype) {
return redisTemplate.getExpire(key, tiemtype);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
return true;
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
this.set(key, value);
}
return true;
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time time 时间类型自定义设定
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time,TimeUnit tiemtype) {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, tiemtype);
} else {
this.set(key, value);
}
return true;
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param delta 要减少几(大于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
redisTemplate.opsForHash().putAll(key, map);
return true;
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
redisTemplate.opsForHash().put(key, item, value);
return true;
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
final Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
return redisTemplate.opsForSet().size(key);
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
final Long count = redisTemplate.opsForSet().remove(key, values);
return count;
}
// ===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 获取list缓存的长度
* @param key 键
*/
public long lGetListSize(String key) {
return redisTemplate.opsForList().size(key);
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
*/
public boolean lSetList(String key, List<Object> value) {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
*/
public boolean lSetList(String key, List<Object> value, long time) {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
*/
public boolean lUpdateIndex(String key, long index, Object value) {
redisTemplate.opsForList().set(key, index, value);
return true;
}
}
IP地址工具类
java
public class IPAddrUtils {
public static HttpServletRequest GetHttpServletRequest(){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
assert servletRequestAttributes != null;
return servletRequestAttributes.getRequest();
}
public static String GetIPAddr() {
HttpServletRequest request = GetHttpServletRequest();
return GetIPAddr(request);
}
public static String GetIPAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
try {
ipAddress = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
// 通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null) {
if (ipAddress.contains(",")) {
return ipAddress.split(",")[0];
} else {
return ipAddress;
}
} else {
return "";
}
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
}
基本上我们的这个工具类就这些。这里之所以给出的话,主要是因为我们这个玩意是通用的。
限流切面
那么在我们开始之前,我们先来实现一下我们一些限流的切面。比如我们那个邮箱验证码接口,你前端60s后访问,有啥用,后端还是可以访问的话,可不行。 所以我们定义一个注解
java
/**
* 默认限制间隔时间是1秒,限制访问
* */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RestrictRequest {
int frequency() default 1;
}
然后写个切面的类
java
/**
* 实现接口限流,这里是针对IP进行限流
* */
@Aspect
@Component
public class RestrictRequestAspect {
private RedisUtils redisUtils;
public RestrictRequestAspect(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Autowired
public void setRedisUtils(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Pointcut("@annotation(com.huterox.hlangserver.anno.RestrictRequest)")
public void verification() {}
@Around("verification()")
public R verification(ProceedingJoinPoint joinPoint) throws Throwable {
//获取方法名
String methodName = joinPoint.getSignature().getName();
String ipAddr = IPAddrUtils.GetIPAddr();
//获取注解的值(暂停值)
Signature signature = joinPoint.getSignature();
MethodSignature msg=(MethodSignature) signature;
Object target = joinPoint.getTarget();
Method method = target.getClass().getMethod(msg.getName(), msg.getParameterTypes());
RestrictRequest annotation = method.getAnnotation(RestrictRequest.class);
int frequency = annotation.frequency();
//进行校验审核
String key = RedisTransKey.getServerRestrict(methodName + ":" + ipAddr + ":");
if(redisUtils.get(key)!=null){
return R.error(BizCodeEnum.OVER_REQUESTS.getCode(),BizCodeEnum.OVER_REQUESTS.getMsg());
}
//设置限流标志
redisUtils.set(key,"1",frequency, TimeUnit.SECONDS);
return (R) joinPoint.proceed();
}
}
同样的,这次我们权限验证都不要第三方框架来做,自己斜切面美滋滋。
注册实现
刚刚在前端我们说了这个基本注册实现,所以我们这边也就是开放两个接口。
java
@Service
public class RegisterServiceImpl implements RegisterService {
private RedisUtils redisUtils;
private HgUserService hgUserService;
private HgInfoService hgInfoService;
private MailService mailService;
@Value("${spring.mail.limit}")
Integer limit;
@Value("${spring.mail.limitTime}")
Integer limitTime;
public RegisterServiceImpl(RedisUtils redisUtils, HgUserService hgUserService, HgInfoService hgInfoService, MailService mailService) {
this.redisUtils = redisUtils;
this.hgUserService = hgUserService;
this.hgInfoService = hgInfoService;
this.mailService = mailService;
}
@Autowired
public void setRedisUtils(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Autowired
public void setHgUserService(HgUserService hgUserService) {
this.hgUserService = hgUserService;
}
@Autowired
public void setHgInfoService(HgInfoService hgInfoService) {
this.hgInfoService = hgInfoService;
}
@Autowired
public void setMailService(MailService mailService) {
this.mailService = mailService;
}
@Override
public R register(RegisterQ registerQ) {
//先验证验证码
Object o = redisUtils.get(RedisTransKey.getServerEmail(registerQ.getUsername()));
EmailCode emailCode = JSON.parseObject(o.toString(), EmailCode.class);
if (emailCode.getUsername().equals(registerQ.getUsername())
&&emailCode.getPassword().equals(registerQ.getPassword())
&&emailCode.getCode().equals(registerQ.getEmailCode())
&&emailCode.getEmail().equals(registerQ.getEmail())
)
{
//存储基本的用户信息
HgInfoEntity hgInfoEntity = new HgInfoEntity();
hgInfoEntity.setNickname(registerQ.getNickname());
hgInfoService.save(hgInfoEntity);
//验证通过创建用户
HgUserEntity hgUserEntity = new HgUserEntity();
hgUserEntity.setPassword(SecurityUtils.encodePassword(registerQ.getPassword()));
hgUserEntity.setUsername(registerQ.getUsername());
hgUserEntity.setEmail(registerQ.getEmail());
//生成userid
hgUserEntity.setUserid(UUIDUtils.get20UUID());
hgUserEntity.setState(UserSateEnum.NORMAL.getState());
hgUserEntity.setInfoid(hgInfoEntity.getInfoid());
hgUserEntity.setUpdateTime(new Date());
hgUserService.save(hgUserEntity);
//删除验证码
redisUtils.del(RedisTransKey.getServerEmail(registerQ.getUsername()));
return R.ok();
}
return R.error(BizCodeEnum.BAD_PUTDATA.getCode(),BizCodeEnum.BAD_PUTDATA.getMsg());
}
@Override
public R emilCode(EmailQ emailQ) {
//生成验证码
String code = CodeUtils.creatCode(6);
EmailCode emailCode = new EmailCode();
emailCode.setCode(code);
emailCode.setPassword(emailQ.getPassword());
emailCode.setUsername(emailQ.getUsername());
emailCode.setEmail(emailQ.getEmail());
//设置10分钟的验证码过期时间
redisUtils.set(RedisTransKey.setServerEmail(emailQ.getUsername()),
emailCode,limitTime, TimeUnit.MINUTES
);
//发送验证码
mailService.sendCodeMailMessage(emailQ.getEmail(), emailCode.getCode());
return R.ok();
}
}
登录实现
之后来看到我们的登录实现,那么这里的话,我们两个部分
- 生成token
- 校验token
当然这里的话,我们用到还是单token,本来我是打算用用双token方案,然后看到无感刷新这几个字之后,一个脱裤子放屁的感觉突然涌上心头。这玩意除了骗骗小白爬虫一点儿用都没有 别人拿到你的token之后,虽然说,你访问接口,或者说验证的时候,用到不是这个token(长期),这个长期token是用来申请临时token的,然后由临时token进行验证。但问题是,我能拿到你的长期token,然后再拿到临时token,通过这个临时token去访问不就完了嘛,你还给我来个无感刷新,好家伙,请问除了让我js debug一下这个麻烦一点还有啥。所以还不如说,你在验证token的时候再验证IP,如果IP不一样直接token作废,如果IP还一样,我有理由怀疑就是你小子本人在爬取网站
生成token
这里的话,我们还有一个token生成类
java
public class JwtTokenUtil {
private static final String secret;
private static final Long expiration;
private static Map<String, Object> header;
static {
secret="Hlang-Huterox";
expiration = 31*24*60*60*1000L;
header=new HashMap<>();
header.put("typ", "jwt");
}
/**
* 生成token令牌
* @return 令token牌
*/
public static String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", user.getUsername());
claims.put("userid",user.getUserid());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* @param token 令牌
* @return 用户名
*/
public static String GetUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = (String) claims.get("username");
} catch (Exception e) {
username = null;
}
return username;
}
public static String GetUserIDFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = (String) claims.get("userid");
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
* @return 是否有效
*/
public static Boolean validateToken(String token, User user) {
String username = GetUserNameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
/**
* 从claims生成令牌,如果看不懂就看谁调用它
*
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + expiration);
return Jwts.builder()
.setHeader(header)
.setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从令牌中获取数据声明,如果看不懂就看谁调用它
*
* @param token 令牌
* @return 数据声明
*/
public static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
}
然后的话,我们拿到登录接口
java
public class LoginServiceImpl implements LoginService {
private HgUserService hgUserService;
private HgInfoService hgInfoService;
private RedisUtils redisUtils;
public LoginServiceImpl(HgUserService hgUserService, HgInfoService hgInfoService, RedisUtils redisUtils) {
this.hgUserService = hgUserService;
this.hgInfoService = hgInfoService;
this.redisUtils = redisUtils;
}
@Autowired
public void setHgUserService(HgUserService hgUserService) {
this.hgUserService = hgUserService;
}
@Autowired
public void setRedisUtils(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Autowired
public void setHgInfoService(HgInfoService hgInfoService) {
this.hgInfoService = hgInfoService;
}
@Override
public R login(LoginQ loginQ) {
String userName = loginQ.getUserName();
String passWord = loginQ.getPassWord();
HgUserEntity userEntity = hgUserService.getOne(
new QueryWrapper<HgUserEntity>().eq("username", userName)
);
if(userEntity!=null){
//签发token
User user = new User();
user.setUserid(userEntity.getUserid());
user.setUsername(userName);
user.setPassword(passWord);
String token = JwtTokenUtil.generateToken(user);
//这里我们直接把IP地址也作为验证的一个条件
Token tokenSave = new Token();
tokenSave.setToken(token);
tokenSave.setIp(IPAddrUtils.GetIPAddr());
tokenSave.setLoginType(loginQ.getType());
if(SecurityUtils.matchesPassword(passWord,userEntity.getPassword())){
//将token存到redis当中
if(loginQ.getType().equals(LoginType.PcType)){
redisUtils.set(RedisTransKey.setServerToken(userEntity.getUserid()+":"+LoginType.PcType),
tokenSave,30, TimeUnit.DAYS);
}else if(loginQ.getType().equals(LoginType.MobileType)){
redisUtils.set(RedisTransKey.setServerToken(userEntity.getUserid()+":"+LoginType.MobileType),
tokenSave,30, TimeUnit.DAYS);
}
//组装返回消息
HgInfoEntity infoEntity = hgInfoService.getById(userEntity.getInfoid());
UserLoginR userLoginR = new UserLoginR();
userLoginR.setUserid(userEntity.getUserid());
userLoginR.setToken(token);
userLoginR.setNickname(infoEntity.getNickname());
userLoginR.setUpic(infoEntity.getUPic());
userLoginR.setLogin(true);
return R.ok(BizCodeEnum.SUCCESSFUL.getMsg()).put("userLoginR",userLoginR);
}
}
return R.error(BizCodeEnum.NO_SUCHUSER.getCode(), BizCodeEnum.NO_SUCHUSER.getMsg());
}
}
验证token
那么这里同样我们是搞了个注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedLogin {
}
然后做验证
java
/**
* 负责专门校验,用户有没有登录用的
* */
@Component
@Aspect
@Slf4j
public class VerificationAspect {
private RedisUtils redisUtils;
public VerificationAspect(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Autowired
public void setRedisUtils(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Pointcut("@annotation(com.huterox.hlangserver.anno.NeedLogin)")
public void verification() {}
/**
* 我们这里再直接抛出异常,反正有那个谁统一异常类
*/
@Around("verification()")
public Object verification(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
assert servletRequestAttributes != null;
HttpServletRequest request = servletRequestAttributes.getRequest();
String ipAddr = IPAddrUtils.GetIPAddr();
//分登录的设备进行验证
String loginType = request.getHeader("loginType");
String userid = request.getHeader("userid");
String tokenUser = request.getHeader("loginToken");
String tokenKey = RedisTransKey.getServerToken(userid + ":" + loginType);
if(tokenUser==null || userid==null || loginType==null){
throw new BadLoginParamsException();
}
if(redisUtils.hasKey(tokenKey)){
if(loginType.equals(LoginType.PcType)){
Object o = redisUtils.get(tokenKey);
Token loginToken = JSON.parseObject(o.toString(), Token.class);
if(!(loginToken.getToken().equals(tokenUser)&&loginToken.getIp().equals(ipAddr))){
throw new BadLoginQException();
}
}else if (loginType.equals(LoginType.MobileType)){
Object o = redisUtils.get(tokenKey);
Token loginToken = JSON.parseObject(o.toString(), Token.class);
if(!(loginToken.getToken().equals(tokenUser)&&loginToken.getIp().equals(ipAddr))){
throw new BadLoginQException();
}
}
}else {
throw new NotLoginException();
}
return proceedingJoinPoint.proceed();
}
}
总结
okey,接下来几天慢慢写基本业务即可,在业务方面我们还有一个netty的消息和在线聊天室要做。之后就是算法层面有个推荐算法。