22.前台系统-用户登录
# 登录需求
登录采取弹出层的形式
登录方式
- 手机号码+手机验证码
- 微信扫码登录
无注册界面,第一次登录根据手机号判断系统是否存在,如果不存在则自动注册
微信扫描登录成功必须绑定手机号码
- 第一次扫描成功后绑定手机号,以后登录扫描直接登录成功
网关统一判断登录状态,若需要登录,页面弹出登录层
页面效果
# 构建服务模块 service-user
搭建过程同service-hosp模块
# 修改pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>service</artifactId>
<groupId>com.stt.yygh</groupId>
<version>0.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<version>1.0</version>
<artifactId>service-user</artifactId>
<packaging>jar</packaging>
<name>service-user</name>
<description>service-user</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.stt.yygh</groupId>
<artifactId>service-cmn-client</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 添加配置文件application.properties
# 服务端口
server.port=8203
# 服务名
spring.application.name=service-user
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/yygh_user?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/stt/yygh/user/mapper/xml/*.xml
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 添加启动类
创建com.stt.yygh.user 包并添加启动类 ServiceUserApplication
package com.stt.yygh.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = "com.stt")
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.stt")
public class ServiceUserApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceUserApplication.class, args);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 添加实体对象 UserInfo
在 model中创建com.stt.yygh.model.user 并添加 UserInfo 实体类
- 实体类对象对应数据库表字段信息
package com.stt.yygh.model.user;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.stt.yygh.model.base.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "UserInfo")
@TableName("user_info")
public class UserInfo extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
@TableField("openid")
private String openid;
@ApiModelProperty(value = "昵称")
@TableField("nick_name")
private String nickName;
@ApiModelProperty(value = "手机号")
@TableField("phone")
private String phone;
@ApiModelProperty(value = "用户姓名")
@TableField("name")
private String name;
@ApiModelProperty(value = "证件类型")
@TableField("certificates_type")
private String certificatesType;
@ApiModelProperty(value = "证件编号")
@TableField("certificates_no")
private String certificatesNo;
@ApiModelProperty(value = "证件路径")
@TableField("certificates_url")
private String certificatesUrl;
@ApiModelProperty(value = "认证状态(0:未认证 1:认证中 2:认证成功 -1:认证失败)")
@TableField("auth_status")
private Integer authStatus;
@ApiModelProperty(value = "状态(0:锁定 1:正常)")
@TableField("status")
private Integer status;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 添加Mapper
创建包 com.stt.yygh.user.mapper 并添加 UserInfoMapper 类
package com.stt.yygh.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.stt.yygh.model.user.UserInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
2
3
4
5
6
7
8
9
创建包com.stt.yygh.user.mapper.xml 并添加UserInfoMapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stt.yygh.user.mapper.UserInfoMapper">
</mapper>
2
3
4
5
6
# 添加service
创建包com.stt.yygh.user.service 并添加UserInfoService接口
package com.stt.yygh.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.stt.yygh.model.user.UserInfo;
public interface UserInfoService extends IService<UserInfo> {
}
2
3
4
5
6
7
创建包com.stt.yygh.user.service.impl 并添加 UserInfoServiceImpl接口实现
package com.stt.yygh.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.stt.yygh.model.user.UserInfo;
import com.stt.yygh.user.mapper.UserInfoMapper;
import com.stt.yygh.user.service.UserInfoService;
import org.springframework.stereotype.Service;
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
}
2
3
4
5
6
7
8
9
10
11
# 添加controller
创建包com.stt.yygh.user.controller 并添加 UserInfoApiController 类
package com.stt.yygh.user.controller;
import com.stt.yygh.user.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserInfoApiController {
@Autowired
private UserInfoService userInfoService;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 配置网关
在 service-gateway服务模块中添加路由
#设置路由id
spring.cloud.gateway.routes[2].id=service-user
#设置路由的uri
spring.cloud.gateway.routes[2].uri=lb://service-user
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[2].predicates=Path=/*/user/**
2
3
4
5
6
# 实现登录接口
# 在model中添加vo类
在model模块中创建 com.stt.yygh.vo.user 包并添加LoginVo类
package com.stt.yygh.vo.user;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description="登录对象")
public class LoginVo {
@ApiModelProperty(value = "openid")
private String openid;
@ApiModelProperty(value = "手机号")
private String phone;
@ApiModelProperty(value = "密码")
private String code;
@ApiModelProperty(value = "IP")
private String ip;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 添加工具类 IpUtil
在 service-user模块中创建 com.stt.yygh.user.utils 包,并添加 IpUtil 类
作用:获取当前请求的真实IP地址
package com.stt.yygh.user.utils;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IpUtil {
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST = "127.0.0.1";
private static final String SEPARATOR = ",";
public static String getIpAddr(HttpServletRequest request) {
System.out.println(request);
String ipAddress;
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 (LOCALHOST.equals(ipAddress)) {
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
// "***.***.***.***".length()
if (ipAddress != null && ipAddress.length() >15) {
if (ipAddress.indexOf(SEPARATOR) >0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress = "";
}
return ipAddress;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 在controller中添加方法
package com.stt.yygh.user.controller;
import com.stt.yygh.common.result.Result;
import com.stt.yygh.user.service.UserInfoService;
import com.stt.yygh.user.utils.IpUtil;
import com.stt.yygh.vo.user.LoginVo;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class UserInfoApiController {
@Autowired
private UserInfoService service;
@ApiOperation(value = "会员登录")
@PostMapping("login")
public Result login(@RequestBody LoginVo loginVo, HttpServletRequest request) {
loginVo.setIp(IpUtil.getIpAddr(request));
Map<String, Object> info = service.login(loginVo);
return Result.ok(info);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 在service中添加方法
在UserInfoService中添加方法
package com.stt.yygh.user.service;
...
public interface UserInfoService extends IService<UserInfo> {
Map<String, Object> login(LoginVo loginVo);
}
2
3
4
5
在UserInfoServiceImpl 中添加响应的实现
- TODO 的表示后期实现
package com.stt.yygh.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.stt.yygh.common.exception.YyghException;
import com.stt.yygh.common.result.ResultCodeEnum;
import com.stt.yygh.model.user.UserInfo;
import com.stt.yygh.user.mapper.UserInfoMapper;
import com.stt.yygh.user.service.UserInfoService;
import com.stt.yygh.vo.user.LoginVo;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
@Override
public Map<String, Object> login(LoginVo loginVo) {
String phone = loginVo.getPhone();
String code = loginVo.getCode();
//校验参数是否合法
if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
//TODO 校验校验验证码
// 通过手机号获取 会员,如果存在则返回,如果不存在则创建
UserInfo userInfo = saveUserInfoIfNotExistsByPhone(phone);
//TODO 记录登录逻辑
Map<String, Object> map = new HashMap<>();
map.put("name", getUserInfoName(userInfo));
map.put("token", ""); // TOOD 后期添加生成token逻辑
return map;
}
//返回页面显示名称,如果存在名称则返回,某则返回昵称,否则返回手机号作为名称
private String getUserInfoName(UserInfo userInfo){
String name = userInfo.getName();
if (StringUtils.isEmpty(name)) {
name = userInfo.getNickName();
}
if (StringUtils.isEmpty(name)) {
name = userInfo.getPhone();
}
return name;
}
// 通过手机号创建UserInfo,如果存在则返回,如果不存在则创建
private UserInfo saveUserInfoIfNotExistsByPhone(String phone) {
//手机号已被使用
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("phone", phone);
//获取会员
UserInfo userInfo = baseMapper.selectOne(queryWrapper);
if(Objects.isNull(userInfo)){
userInfo = new UserInfo();
userInfo.setName("");
userInfo.setPhone(phone);
userInfo.setStatus(1);
this.save(userInfo);
}
//校验是否被禁用
if (userInfo.getStatus() == 0) {
throw new YyghException(ResultCodeEnum.LOGIN_DISABLED_ERROR);
}
return userInfo;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
说明
- 验证码先注释,后续校验
- 登录成功生成token,后续讲解
# 测试
使用swagger2测试接口
# 生成token
完成登录时,生成一个字符串,包含用户信息(如用户名,或者用户名+ip+xxx等信息组合)以及有效时间等;每次请求将该信息生成的token放入到请求头header中传递给后端,后端判断查看是否包含token,包含则校验token检验登录状态,不包含则直接返回登录页面
本项目使用JWT实现该token的生成
# 关于JWT
JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源
- 如用在用户登录上
JWT最重要的作用就是对 token信息的防伪作用
由三个部分组成:公共部分、私有部分、签名部分。最后由这三者组合进行base64编码得到JWT
# 结构说明
公共部分:主要是该JWT的相关配置参数,比如签名的加密算法、格式类型、过期时间等等
- Key=STT
私有部分:用户自定义的内容,根据实际需要真正要封装的信息
- userInfo{用户的Id,用户的昵称nickName}
签名部分
- SaltiP: 当前服务器的Ip地址!{linux 中配置代理服务器的ip}
- 主要用户对JWT生成字符串的时候,进行加密{盐值}
最终组成 key+salt+userInfo -> token!
- base64编码,并不是加密,只是把明文信息变成了不可见的字符串
- 可使用在线工具把base64编码解成明文
- 不要在JWT中放入涉及私密的信息
# 集成JWT
# 添加依赖
在 common-util模块中修改pom文件,增加依赖,注:版本已在yygh-parent父模块pom.xml添加
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common</artifactId>
<groupId>com.stt.yygh</groupId>
<version>0.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-util</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<name>common-util</name>
<description>common-util</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
...
<!-- JWT工具 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
</project>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 编写工具类
在 common-util中创建编写com.stt.yygh.common.helper.JwtHelper类
package com.stt.yygh.common.helper;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.util.Date;
public class JwtHelper {
// 过期时间 24小时
private static long tokenExpiration = 24*60*60*1000;
// key长度不能太小,否则不符合规范,报错
private static String tokenSignKey = "yygh-jwt";
public static String createToken(Long userId, String userName) {
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.claim("userName", userName)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
public static Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}
public static String getUserName(String token) {
if(StringUtils.isEmpty(token)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String)claims.get("userName");
}
public static void main(String[] args) {
String token = JwtHelper.createToken(1L, "55");
System.out.println(token);
System.out.println(JwtHelper.getUserId(token));
System.out.println(JwtHelper.getUserName(token));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
可执行main方法测试
# 完善登录service接口
修改service-user 模块的UserInfoServiceImpl 类
package com.stt.yygh.user.service.impl;
...
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
@Override
public Map<String, Object> login(LoginVo loginVo) {
...
String name = getUserInfoName(userInfo);
Map<String, Object> map = new HashMap<>();
map.put("name",name);
// jwt生成token字符串
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token",token);
return map;
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用swagger2进行测试