目录
- 完成注册功能
- 后端开发完成
- UserController
- UserServiceImpl
- LogininfoMapper
- 前端页面完成
- 绑定数据
- 绑定事件
- 准备登录页
- 管理员登录1
- 需求分析
- 登录设计
- 页面设计
- 表设计
- 流程设计
- 所需技术
- 员工新增级联操作登录信息
- EmployeeServiceImpl
- ShopServiceImpl
- 管理员登录2
- 前端页面
- 后端接口
- LoginController
- LoginServiceImpl
- logininfoMapper
- 前端登录完成
- 后端拦截
- PetHomeWebMvcConfigurer
- LoginInterceptor
- 前端拦截
- 路由拦截
- 其他
分别启动后端,后台,前台和redis
ph-admin:npm run dev,ph-web:live-server --port=80
redis:redis-server.exe redis.windows.conf
完成注册功能
后端开发完成
UserController
/**
* 用户注册接口
* @param userDto 自定义Dto接收前端参数
* @return
*/
@PostMapping("/register/code")
public AjaxResult redisterCode(@RequestBody UserDto userDto){
try {
userService.redisterCode(userDto);
return AjaxResult.me();
} catch (BusinessException e) {
return AjaxResult.me().setMessage(e.getMessage());
}catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setMessage("系统繁忙,稍后重试!!");
}
}
UserServiceImpl
@Override
public void redisterCode(UserDto userDto) {
//1.校验
// 1.1空校验
if(StringUtils.isEmpty(userDto.getPhone()) ||
StringUtils.isEmpty(userDto.getVerifycode()) ||
StringUtils.isEmpty(userDto.getPassword()) ||
StringUtils.isEmpty(userDto.getConfirmPwd()) ){
throw new BusinessException("请输入完整信息在进行注册!");
}
// 1.2两次密码是否一致
if(!userDto.getPassword().equals(userDto.getConfirmPwd())){
throw new BusinessException("密码不一致,请重新输入!");
}
// 1.3是否被注册
Logininfo obj = logininfoMapper.loadByDto(userDto);
if(obj != null){
throw new BusinessException("用户已经存在!");
}
//2.验证码是否存在
Object codeObj = redisTemplate.opsForValue().get(UserConstant.USER_VERFIY_CODE+":"+userDto.getPhone());
// 2.1 验证码是否过期
if(codeObj == null){
throw new BusinessException("验证码已经过期,请重新发送验证码!");
}
String code = ((String)codeObj).split(":")[0];
// 2.2 验证码是否正确
if(!userDto.getVerifycode().equalsIgnoreCase(code)){
throw new BusinessException("请输入正确的验证码!");
}
//3.登录成功
// 3.1登录信息表
User user = userDto2User(userDto);
Logininfo logininfo = user2LoginInfo(user);
logininfoMapper.save(logininfo);//返回自增id
// 3.2用户表
user.setInfo(logininfo);//引用属性传值
userMapper.save(user);
}
/**
* 将dto转换成实体对象
* @param userDto
* @return
*/
private User userDto2User(UserDto userDto) {
//1.定义需要返回的对象
User user = new User();
//2.封装数据
user.setUsername(userDto.getPhone());
user.setPhone(userDto.getPhone());
/*private String salt;
private String password;*/
String salt = StrUtils.getComplexRandomString(32);
String md5Pwd = MD5Utils.encrypByMd5(userDto.getPassword() + salt);
user.setSalt(salt);//存储加密使用的盐值
user.setPassword(md5Pwd);//存储使用盐值加密之后的密码
//3.返回
return user;
}
/**
* 通过user拷贝LoginInfo
* @param user
* @return
*/
private Logininfo user2LoginInfo(User user) {
Logininfo info = new Logininfo();
//直接使用工具类拷贝 同名原则拷贝属性值
BeanUtils.copyProperties(user, info);
info.setType(1);//设置前端用户默认值
return info;
}
LogininfoMapper
<!--Logininfo loadByDto(UserDto userDto);-->
<select id="loadByDto" parameterType="cn.itsource.user.dto.UserDto" resultType="Logininfo">
SELECT * FROM t_logininfo WHERE phone = #{phone} and type = #{type}
</select>
前端页面完成
绑定数据
data:{
phoneUserForm:{
phone:"",
verifycode:"",
password:"1",
confirmPwd:"1",
type:1
}
},
<form method="post">
<div class="user-phone">
<label for="phone"><i class="am-icon-mobile-phone am-icon-md"></i></label>
<input type="tel" v-model="phoneUserForm.phone" name="" id="phone" placeholder="请输入手机号">
</div>
<div class="verification">
<label for="code"><i class="am-icon-code-fork"></i></label>
<input type="tel" name="" v-model="phoneUserForm.verifycode" id="code" placeholder="请输入验证码">
<!--<a class="btn" href="javascript:void(0);" οnclick="sendMobileCode();" id="sendMobileCode">
<span id="dyMobileButton">获取</span></a>-->
<button type="button" @click="sendMobileCode">获取</button>
</div>
<div class="user-pass">
<label for="password"><i class="am-icon-lock"></i></label>
<input type="password" name="" v-model="phoneUserForm.password" id="password" placeholder="设置密码">
</div>
<div class="user-pass">
<label for="passwordRepeat"><i class="am-icon-lock"></i></label>
<input type="password" name="" v-model="phoneUserForm.confirmPwd" id="passwordRepeat" placeholder="确认密码">
</div>
</form>
绑定事件
<div class="am-cf">
<input type="submit" name="" @click="register" value="注册" class="am-btn am-btn-primary am-btn-sm am-fl">
</div>
register(){
this.$http.post("/user/register/code",this.phoneUserForm)
.then(result=>{
result = result.data;
if(result.success){
//成功之后? 提示成功
alert("注册成功!");
//跳转到登录页
location.href="login.html";
}else{
alert("注册失败:"+result.message);
}
})
.catch(result=>{
alert("系统异常!");
})
},
准备登录页
拷贝login.html到根目录并修改路径
管理员登录1
需求分析,页面,表设计,流程设计,
需求分析
见文档
登录设计
页面设计
见文档
表设计
见文档
流程设计
见文档
所需技术
见文档
员工新增级联操作登录信息
店铺入驻保存shop表和employee表的时候要同步存入logininfo表
员工新增保存employee表的时候也要保存logininfo表
做法:分别在EmployeeServiceImpl和UserServiceImpl重写add,update,del方法,方法内加入同步操作logininfo表的代码
EmployeeServiceImpl
/**
* 针对于loginInfo的级联操作:员工和用户都应该有
* 这里我们只写 员工的
*/
@Override
@Transactional
public void add(Employee employee) {
initEmployee(employee);
Logininfo logininfo = employee2LoginInfo(employee);
logininfoMapper.save(logininfo);
employee.setLogininfo(logininfo);
employeeMapper.save(employee);
}
@Override
public void update(Employee employee) {
//1.修改loginInfo
Logininfo logininfo = logininfoMapper.loadById(employee.getLogininfo_id());
if(logininfo != null){
BeanUtils.copyProperties(employee, logininfo);
logininfoMapper.update(logininfo);
employeeMapper.update(employee);
}
}
@Override
public void delete(Long id) {
Employee employee = employeeMapper.loadById(id);
if(employee != null){
logininfoMapper.remove(employee.getLogininfo_id());
employeeMapper.remove(id);
}
}
private void initEmployee(Employee employee) {
//设置盐值
String salt = StrUtils.getComplexRandomString(32);
String md5Pwd = MD5Utils.encrypByMd5(employee.getPassword()+salt);
employee.setSalt(salt);
employee.setPassword(md5Pwd);
}
private Logininfo employee2LoginInfo(Employee employee) {
Logininfo logininfo = new Logininfo();
BeanUtils.copyProperties(employee, logininfo);
logininfo.setType(0);
return logininfo;
}
ShopServiceImpl
//2.先保存员工数据 保存之后返回自增id
// employeeMapper.save(admin);
employeeService.add(admin);
管理员登录2
前端页面
ph-admin - login.vue
标签元素 - data - 按钮 - methods
<template>
<el-form :model="ruleForm2" :rules="rules2" ref="ruleForm2" label-position="left" label-width="0px" class="demo-ruleForm login-container">
<h3 class="title">系统登录</h3>
<el-form-item prop="account">
<el-input type="text" v-model="ruleForm2.username" auto-complete="off" placeholder="账号"></el-input>
</el-form-item>
<el-form-item prop="checkPass">
<el-input type="password" v-model="ruleForm2.password" auto-complete="off" placeholder="密码"></el-input>
</el-form-item>
<el-checkbox v-model="checked" checked class="remember">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button type="primary" style="width:47%;" @click.native.prevent="login" :loading="logining">登录</el-button>
<el-button type="success" style="width:47%;" @click.native.prevent="goRegister">店铺入驻</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { requestLogin } from '../api/api';
//import NProgress from 'nprogress'
export default {
data() {
return {
logining: false,
ruleForm2: {
username: '3',
password: '3',
type:0//表名是后端员工登录
},
rules2: {
account: [
{ required: true, message: '请输入账号', trigger: 'blur' },
//{ validator: validaePass }
],
checkPass: [
{ required: true, message: '请输入密码', trigger: 'blur' },
//{ validator: validaePass2 }
]
},
checked: true
};
},
methods: {
goRegister(){//这里是拷贝的登录成功只有跳转主页
this.$router.push({ path: '/shopRegister' });/*店铺入驻*/
},
handleReset2() {
this.$refs.ruleForm2.resetFields();
},
login(ev) {
this.$refs.ruleForm2.validate((valid) => { //点击登录按钮的时候,触发表单校验
if (valid) {
this.logining = true; //开启忙等框
this.$http.post("/login/account",this.ruleForm2)
.then(result=>{
this.logining = false;//关闭忙等框
result = result.data;
if(result.success){
//提示登录成功
this.$message({
message: "登录成功!",
type: 'success'
});
//跳转到主页
this.$router.push({ path: '/echarts' });
}else{
this.$message({
message: result.message,
type: 'error'
});
}
})
.catch(result=>{
this.logining = false;//关闭忙等框
this.$message({
message: "系统异常",
type: 'error'
});
})
} else {
console.log('error submit!!');
return false;
}
});
}
}
}
</script>
后端接口
LoginController
/**
* 统一登录接口
*/
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
ILoginService loginService;
/**
* 账号登录,支持前后端账号登录
*/
@PostMapping("/account")
public AjaxResult account(@RequestBody LoginDto loginDto){
try {
return loginService.account(loginDto);
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setMessage("系统繁忙,稍后重试!");
}
}
}
LoginServiceImpl
/**
* 统一登录业务层
*/
@Service
@Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
public class LoginServiceImpl implements ILoginService {
@Autowired
private LogininfoMapper logininfoMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 账号登录
* loginDto 中表明了是哪一个端登录 type = 0
*/
@Override
public AjaxResult account(LoginDto loginDto) {
//1.校验
// 1.1 空校验 loginDto所有字段都要校验
if(StringUtils.isEmpty(loginDto.getUsername()) ||
StringUtils.isEmpty(loginDto.getPassword()) ||
StringUtils.isEmpty(loginDto.getType()) ){
return AjaxResult.me().setMessage("用户或密码不能为空!");
}
// 1.2 判断用户是否存在 查询loginInfo表
Logininfo logininfo = logininfoMapper.loadByLoginDto(loginDto);
if (logininfo == null) {
return AjaxResult.me().setMessage("用户不存在!");
}
// 1.3 账号是否被禁用
if(logininfo.getDisable() != 1){
return AjaxResult.me().setMessage("账号被禁用,请联系管理员!");
}
// 2.判断密码是否正确
String salt = logininfo.getSalt();
String md5pwd = logininfo.getPassword();
String md5PwdTmp = MD5Utils.encrypByMd5(loginDto.getPassword() + salt);
if(!md5pwd.equals(md5PwdTmp)){
// 2.2 密码不等 抛错
return AjaxResult.me().setMessage("用户名或密码错误!");
}
// 2.1 如果密码相等 登录成功 存redis,封装返回值
/**
* 这里的redis key就是前端需要存储的token
*/
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(token, logininfo, 30, TimeUnit.MINUTES);
Map<String,Object> map = new HashMap<>();
map.put("token", token);
map.put("logininfo",logininfo);
return AjaxResult.me().setResultObj(map);
}
}
logininfoMapper
<select id="loadByLoginDto" parameterType="cn.ming.basic.dto.LoginDto" resultType="Logininfo">
SELECT * FROM t_logininfo
WHERE (username = #{username} or phone = #{username} or email = #{username})
and type = #{type}
</select>
前端登录完成
保存信息到localstorage
login(ev) {
this.$refs.ruleForm2.validate((valid) => { //点击登录按钮的时候,触发表单校验
if (valid) {
this.logining = true; //开启忙等框
this.$http.post("/login/account",this.ruleForm2)
.then(result=>{
this.logining = false;//关闭忙等框
result = result.data;
if(result.success){
//保存信息到localstorage
var resultObj = result.resultObj;
localStorage.setItem("token",resultObj.token);
localStorage.setItem("logininfo",JSON.stringify(resultObj.logininfo));
//提示登录成功
this.$message({
message: "登录成功!",
type: 'success'
});
//跳转到主页
this.$router.push({ path: '/echarts' });
}else{
this.$message({
message: result.message,
type: 'error'
});
}
})
.catch(result=>{
this.logining = false;//关闭忙等框
this.$message({
message: "系统异常",
type: 'error'
});
})
} else {
console.log('error submit!!');
return false;
}
});
}
前端右上角展示登录人信息
mounted() {
var user = localStorage.getItem('logininfo');
if (user) {
user = JSON.parse(user);
this.sysUserName = user.username || user.email || user.phone || '';
this.sysUserAvatar = user.avatar || '';
}
}
后端拦截
除了登录和注册相关的其他所有ajax请求都应该拦截
如:注册,发送验证码,店铺入驻,图片上传,
PetHomeWebMvcConfigurer
@Configuration
public class PetHomeWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login/**")
.excludePathPatterns("/user/register/**")
.excludePathPatterns("/verifycode/**")
.excludePathPatterns("/fastDfs")
.excludePathPatterns("/shop/settlement");
}
}
LoginInterceptor
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
//判断是否登录? 前端需要给我们传递 token
String token = request.getHeader("token");// fde6185f-36ac-4df0-9c0e-9d4c4ebb617c
if(!StringUtils.isEmpty(token)){
Object obj = redisTemplate.opsForValue().get(token);
if(obj !=null){//已经登录过
//刷新redis的token存储时间
redisTemplate.opsForValue().set(token, obj, 30, TimeUnit.MINUTES);
//放行
return true;
}
}
//如果已经登录过期
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = response.getWriter();
writer.write("{\"success\":false,\"message\":\"noLogin\"}");
writer.flush();
writer.close();
//@TODO 权限校验
return false;
}
}
前端拦截
axios前置拦截:发送axios请求前给请求加上token
axios后置拦截:接收到后端拦截器的nologin信息后跳到登录页
写在main.js,vue下面
/**axios前置拦截器
*作用:每次发送axios请求,需要携带token给后端
*/
axios.interceptors.request.use(config=>{
//携带token
let uToken = localStorage.getItem("token");
if(uToken){
config.headers['token']=uToken;
}
return config;//一定要返回配置
},error => {
Promise.reject(error);
})
/**
* axios后置拦截: 作用:接收后端拦截器报错,在这里处理
*/
axios.interceptors.response.use(result=>{
console.log(result.data+"jjjjjjj");
let data = result.data;
if(!data.success && data.message==="noLogin"){
localStorage.removeItem('token');
localStorage.removeItem('logininfo');
router.push({ path: '/login' });
}
return result;
},error => {
Promise.reject(error);
})
路由拦截
http://localhost:8081/#/
根首页并没有触发axios请求,是直接访问直接打开的,也需要拦截
登录和注册页面要放行
router.beforeEach((to, from, next) => {
if (to.path == '/login' || to.path == '/shopRegister') {
localStorage.removeItem('token');
localStorage.removeItem('logininfo');
next()//需要放行
}else{
let user = JSON.parse(localStorage.getItem('logininfo'));
if (!user) {//如果没有值,定位到登录页
next({ path: '/login' })
} else {//如果有值,正常访问页面
next()
}
}
})
其他
管理员登录
前提:login.vue中集成axios和vue
整体思路 先不要考虑登录拦截,先把登录做好
1)后台登录接口
2)前台登录实现并且保存loginInfo和token到localStorage,登录成功跳转首页,并展示用户名
3)前台通过axios的前置拦截器携带token到后台
4)后台做token的登录拦截器,如果没有回报错给前台
5)前台通过axios后置拦截器对后台登录拦截错误进行跳转到登录页面
6)前台也要做拦截-有的地址是不需要访问后台
后端接口
LoginDto
LoginController /login/acount loginAccount
LoginInfoServiceImpl loginAcount
xml
后台登录
Login.vue /login/account
axios携带token到后台 main.js
为了后端校验是否已经登录,只要用axios的请求都要携带token
后端登录拦截
写拦截器 配置拦截器
前台对后台拦截结果跳转登录页面 main.js 后台拦截器实现后端已经退出登录的跳转登录页面
前端拦截器-页面没有和后台进行数据交互 登录和注册在没有登录情况下也能访问