数据准备:
- sql建表语句
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL COMMENT '昵称',
`pwd` varchar(124) DEFAULT NULL COMMENT '密码',
`head_img` varchar(524) DEFAULT NULL COMMENT '头像',
`phone` varchar(64) DEFAULT '' COMMENT '⼿机号',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `phone` (`phone`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
- 实体类
/**
* `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
* `name` varchar(128) DEFAULT NULL COMMENT '昵称',
* `pwd` varchar(124) DEFAULT NULL COMMENT '密码',
* `head_img` varchar(524) DEFAULT NULL COMMENT '头像',
* `phone` varchar(64) DEFAULT '' COMMENT '手机号',
* `create_time` datetime DEFAULT NULL COMMENT '创建时间',
*/
public class User {
private Integer id;
private String name;
@JsonIgnore
private String pwd;
@JsonProperty("head_img")
private String headImg;
private String phone;
@JsonProperty("create_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", pwd='" + pwd + '\'' +
", headImg='" + headImg + '\'' +
", phone='" + phone + '\'' +
", createTime=" + createTime +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public String getHeadImg() {
return headImg;
}
public void setHeadImg(String headImg) {
this.headImg = headImg;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
规范controller层返回数据:
public class JsonData {
/**
* 状态码 0表示成功过,1表示处理中,-1 表示失败
*/
private Integer code;
/**
* 业务数据
*/
private Object data;
/**
* 信息表示
*/
private String msg;
public JsonData(){}
public JsonData(Integer code, Object data, String msg){
this.code = code;
this.data = data;
this.msg = msg;
}
/**
* 成功,不用返回数据
* @return
*/
public static JsonData buildSuccess(){
return new JsonData(0,null,null);
}
/**
* 成功,返回数据
* @param data
* @return
*/
public static JsonData buildSuccess(Object data){
return new JsonData(0,data,null);
}
/**
* 失败,固定状态码
* @param msg
* @return
*/
public static JsonData buildError(String msg){
return new JsonData(-1 ,null,msg);
}
/**
* 失败,自定义错误码和信息
* @param code
* @param msg
* @return
*/
public static JsonData buildError(Integer code , String msg){
return new JsonData(code ,null,msg);
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
MD5加密
MD5加密⼯具类封装(不⽤死记,不常用的⽅法,直接拷⻉使⽤就⾏,基本是写⼀次,其他项⽬直接拷⻉)
/**
* 工具类
*/
public class CommonUtils {
/**
* MD5加密工具类
* @param data
* @return
*/
public static String MD5(String data) {
try {
java.security.MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
} catch (Exception exception) {
}
return null;
}
}
用户注册功能开发
- Controller层
@RestController
@RequestMapping("api/v1/pri/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 注册接口
*
* @param userInfo
* @return
*/
@PostMapping("register")
public JsonData register(@RequestBody Map<String, String> userInfo) {
int rows = userService.save(userInfo);
return rows == 1 ? JsonData.buildSuccess() : JsonData.buildError("注册失败,请重试");
}
}
- Service层(对密码进行MD5加密入库)
public interface UserService {
/**
* 新增用户
* @param userInfo
* @return
*/
int save(Map<String, String> userInfo);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public int save(Map<String, String> userInfo) {
User user = parseToUser(userInfo);
if( user != null){
return userMapper.save(user);
}else {
return -1;
}
}
private User parseToUser(Map<String,String> userInfo) {
if(userInfo.containsKey("phone") && userInfo.containsKey("pwd") && userInfo.containsKey("name")){
User user = new User();
user.setName(userInfo.get("name"));
user.setHeadImg("");
user.setCreateTime(new Date());
user.setPhone(userInfo.get("phone"));
String pwd = userInfo.get("pwd");
//MD5加密
user.setPwd(CommonUtils.MD5(pwd));
return user;
}else {
return null;
}
}
}
- Dao层:
public interface UserMapper {
int save(User user);
}
<?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.study.mapper.UserMapper">
<insert id="save" parameterType="User">
INSERT INTO user (name, pwd, head_img, phone , create_time)
values (#{name,jdbcType=VARCHAR}, #{pwd,jdbcType=VARCHAR}, #{headImg,jdbcType=VARCHAR},
#{phone,jdbcType=VARCHAR},#{createTime,jdbcType=TIMESTAMP})
</insert>
</mapper>
- 测试
JWT登录校验
**简介:讲解单机和分布式应⽤下登录校验,session共享,分布式缓存使⽤ **
- 单机tomcat应⽤登录检验
- sesssion保存在浏览器和应⽤服务器会话之间
- ⽤户登录成功,服务端会保存⼀个session,当然客户端有⼀个sessionId
- 客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId
- 分布式应⽤中session共享
- 真实的应⽤不可能单节点部署,所以就有个多节点登录session共享的问题需要解决
- tomcat⽀持session共享,但是有⼴播⻛暴;⽤户量⼤的时候,占⽤资源就严重,不推荐
- 使⽤redis存储token:
- 服务端使⽤UUID⽣成随机64位或者128位token,放⼊redis中,然后返回给客户端并存 储在cookie中
- ⽤户每次访问都携带此token,服务端去redis中校验是否有此⽤户即可
![image.png](https://img-blog.csdnimg.cn/img_convert/7736ffc92da88a5d938fb23417f1eae6.png#clientId=u102003f8-f1bc-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=792&id=u423da16e&margin=[object Object]&name=image.png&originHeight=1188&originWidth=1295&originalType=binary&ratio=1&rotation=0&showTitle=false&size=142218&status=done&style=none&taskId=u51575bde-7da8-408c-be1a-2b4e1caa515&title=&width=863.3333333333334)
**简介:分布式应⽤的登录检验解决⽅案 JWT讲解 json wen token **
什么是JWT
- JWT 是⼀个开放标准,它定义了⼀种⽤于简洁,⾃包含的⽤于通信双⽅之间以 JSON 对象的形式安全传递信息的⽅法。 可以使⽤ HMAC 算法或者是 RSA 的公钥密钥对进⾏签名
- 简单来说: 就是通过⼀定规范来⽣成token,然后可以通过解密算法逆向解密token,这样就可以获取⽤户信息
- 优点
- ⽣产的token可以包含基本信息,⽐如id、⽤户昵称、头像等信息,避免再次查库
- 存储在客户端,不占⽤服务端的内存资源
- 缺点
- token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如⽤户权限,密码等
- 如果没有服务端存储,则不能做登录失效处理,除⾮服务端改秘钥
{
id:888,
name:'⼩D',
expire:10000
}
funtion 加密(object, appsecret){
xxxx
return base64( token);
}
function 解密(token ,appsecret){
xxxx
//成功返回true,失败返回false
}
- JWT格式组成 头部、负载、签名
- header+payload+signature
- 头部:主要是描述签名算法
- 负载:主要描述是加密对象的信息,如⽤户的id等,也可以加些规范⾥⾯的东⻄,如iss签发者,exp 过期时间,sub ⾯向的⽤户
- 签名:主要是把前⾯两部分进⾏加密,防⽌别⼈拿到token进⾏base解密后篡改token
- header+payload+signature
- 关于jwt客户端存储
- 可以存储在cookie,localstorage和sessionStorage⾥⾯
导入JWT依赖
<!-- JWT相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
用户登录接口整合JWT登录校验:
- LoginRequest
/**
* 登录 request
*/
public class LoginRequest {
private String phone;
private String pwd;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
- controller
@RestController
@RequestMapping("api/v1/pri/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 注册接口
*
* @param userInfo
* @return
*/
@PostMapping("register")
public JsonData register(@RequestBody Map<String, String> userInfo) {
int rows = userService.save(userInfo);
return rows == 1 ? JsonData.buildSuccess() : JsonData.buildError("注册失败,请重试");
}
/**
* 登录接口
*
* @param loginRequest
* @return
*/
@PostMapping("login")
public JsonData login(@RequestBody LoginRequest loginRequest) {
String token = userService.findByPhoneAndPwd(loginRequest.getPhone(), loginRequest.getPwd());
return token == null ? JsonData.buildError("登录失败,账号密码错误") : JsonData.buildSuccess(token);
}
}
- service
public interface UserService {
/**
* 新增用户
*
* @param userInfo
* @return
*/
int save(Map<String, String> userInfo);
String findByPhoneAndPwd(String phone, String pwd);
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public int save(Map<String, String> userInfo) {
User user = parseToUser(userInfo);
if (user != null) {
return userMapper.save(user);
} else {
return -1;
}
}
@Override
public String findByPhoneAndPwd(String phone, String pwd) {
User user = userMapper.findByPhoneAndPwd(phone, CommonUtils.MD5(pwd));
if (user == null) {
return null;
} else {
String token = JWTUtils.geneJsonWebToken(user);
return token;
}
}
}
- JWT
Jwt工具类
注意点:
1、生成的token, 是可以通过base64进行解密出明文信息
2、base64进行解密出明文信息,修改再进行编码,则会解密失败
3、无法作废已颁布的token,除非改秘钥
认证流程
a.首先,前端通过web表单将自己的用户民和密码发送到后端的接口,这一过程一般是一个HTTP,POST请求。建议的方式是通过SSL加密的方式传输(https协议),从而避免敏感信息被嗅探。
b.后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(token),形成的JWT就是一个形同111.zzz.xxx的字符串, token,head ,payload, singurater
c.后端将JWT字符串 作为登录成功的返回结果返回给前端,前端可以返回的结果保存在localStorage 或sessionStorage上,退出登录时前端删除保存的JWT即可。
d.前端在每次请求时将JWT放入HTTP Header中的Authoriztion 位。(放在Header中的原因是解决XSS和XSRF问题) HEADER.
e.后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)
f。验证通过后,后端使用JWT包含的用户信息进行其他逻辑操作,返回相应结果。
JWT优势
- 可以通过URL,POST参数或者在HTTP,header发送 ,因为数据量小,传输速度也很快。
- 自包含(self-contained): 负载中包含了所有用户所需要的信息,避免了多次查询数据库。
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
public class JWTUtils {
/**
* 过期时间,设置一周
*/
private static final long EXPIRE = 60000 * 60 * 24 * 7;
//private static final long EXPIRE = 1;
/**
* 加密秘钥 自由设置
*/
private static final String SECRET = "xdclass.net168";
/**
* 令牌前缀 自由设置(在生成的令牌前加上前缀,也可不加)
*/
private static final String TOKEN_PREFIX = "xdclass";
/**
* subject 自由设置
*/
private static final String SUBJECT = "xdclass";
/**
* 根据用户信息,生成令牌
* @param user
* @return
*/
public static String geneJsonWebToken(User user){
String token = Jwts.builder().setSubject(SUBJECT)
.claim("head_img",user.getHeadImg())
.claim("id",user.getId())
.claim("name",user.getName())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.signWith(SignatureAlgorithm.HS256,SECRET).compact();
token = TOKEN_PREFIX + token;
return token;
}
/**
* 校验token的方法
* @param token
* @return
*/
public static Claims checkJWT(String token){
try{
final Claims claims = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX,"")).getBody();
return claims;
}catch (Exception e){
return null;
}
}
}
- dao层
public interface UserMapper {
int save(User user);
User findByPhoneAndPwd(@Param("phone") String phone, @Param("pwd") String pwd);
}
<?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="net.xdclass.online_xdclass.mapper.UserMapper">
<insert id="save" parameterType="User">
INSERT INTO user (name, pwd, head_img, phone , create_time)
values (#{name,jdbcType=VARCHAR}, #{pwd,jdbcType=VARCHAR}, #{headImg,jdbcType=VARCHAR},
#{phone,jdbcType=VARCHAR},#{createTime,jdbcType=TIMESTAMP})
</insert>
<!--根据手机号和密码找用户-->
<select id="findByPhoneAndPwd" resultType="User">
select * from user where phone =#{phone} and pwd = #{pwd}
</select>
</mapper>
测试:
登录拦截器
SpringBoot实现登录拦截的原理
SpringBoot通过实现HandlerInterceptor接⼝实现拦截器,通过实现WebMvcConfigurer接⼝实现⼀个配置类,在配置类中注⼊拦截器,最后再通过@Configuration注解注⼊配置.
1.1、实现HandlerInterceptor接⼝
实现HandlerInterceptor接⼝需要实现3个⽅法:
- preHandle
- postHandle
- afterCompletion
public class LoginInterceptor implements HandlerInterceptor {
/**
* 进入到controller之前的方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
String accessToken = request.getHeader("token");
if (accessToken == null) {
accessToken = request.getParameter("token");
}
if (StringUtils.isNotBlank(accessToken)) {
Claims claims = JWTUtils.checkJWT(accessToken);
if (claims == null) {
//告诉登录过期,重新登录
sendJsonMessage(response, JsonData.buildError("登录过期,重新登录"));
return false;
}
Integer id = (Integer) claims.get("id");
String name = (String) claims.get("name");
request.setAttribute("user_id", id);
request.setAttribute("name", name);
return true;
}
}catch (Exception e){}
sendJsonMessage(response, JsonData.buildError("登录过期,重新登录"));
return false;
}
/**
* 响应json数据给前端
* @param response
* @param obj
*/
public static void sendJsonMessage(HttpServletResponse response, Object obj){
try{
ObjectMapper objectMapper = new ObjectMapper();
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print(objectMapper.writeValueAsString(obj));
writer.close();
response.flushBuffer();
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
preHandle在Controller之前执⾏,因此拦截器的功能主要就是在这个部分实现:
- 检查session中是否有user对象存在;
- 如果存在,就返回true,那么Controller就会继续后⾯的操作;
- 如果不存在,就会重定向到登录界⾯。就是通过这个拦截器,使得Controller在执⾏之前,都执⾏⼀遍preHandle.
1.2、实现WebMvcConfigurer接⼝,注册拦截器
实现WebMvcConfigurer接⼝来实现⼀个配置类,将上⾯实现的拦截器的⼀个对象注册到这个配置类中.
/**
* 拦截器配置
*
* 不用权限可以访问url /api/v1/pub/
* 要登录可以访问url /api/v1/pri/
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
LoginInterceptor loginInterceptor(){
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor()).addPathPatterns("/api/v1/pri/*/*/**")
//不拦截哪些路径 斜杠一定要加
.excludePathPatterns("/api/v1/pri/user/login","/api/v1/pri/user/register");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
将拦截器注册到了拦截器列表中,并且指明了拦截哪些访问路径,不拦截哪些访问路径,不拦截哪些资源⽂件;最后再以@Configuration注解将配置注⼊。
1.3、保持登录状态
只需⼀次登录,如果登录过,下⼀次再访问的时候就⽆需再次进⾏登录拦截,可以直接访问⽹站⾥⾯的内容了。在正确登录之后,就将token保存到HTTP Header中,再次访问页⾯的时候,登录拦截器就可以找到这个token(token里包含了用户信息),就不需要再次拦截到登录界⾯了.
使用token个⼈信息查询接⼝开发
**简介:根据token查询个⼈信息接⼝开发 **
直接解密token,获取个⼈信息
通过token解密查询数据库获取个⼈信息
- controller
@RestController
@RequestMapping("api/v1/pri/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 注册接口
*
* @param userInfo
* @return
*/
@PostMapping("register")
public JsonData register(@RequestBody Map<String, String> userInfo) {
int rows = userService.save(userInfo);
return rows == 1 ? JsonData.buildSuccess() : JsonData.buildError("注册失败,请重试");
}
/**
* 登录接口
*
* @param loginRequest
* @return
*/
@PostMapping("login")
public JsonData login(@RequestBody LoginRequest loginRequest) {
String token = userService.findByPhoneAndPwd(loginRequest.getPhone(), loginRequest.getPwd());
return token == null ? JsonData.buildError("登录失败,账号密码错误") : JsonData.buildSuccess(token);
}
/**
* 根据用户id查询用户信息
* @param request
* @return
*/
@GetMapping("find_by_token")
public JsonData findUserInfoByToken(HttpServletRequest request){
Integer userId = (Integer) request.getAttribute("user_id");
if(userId == null){
return JsonData.buildError("查询失败");
}
User user = userService.findByUserId(userId);
return JsonData.buildSuccess(user);
}
}
- service
public interface UserService {
/**
* 新增用户
* @param userInfo
* @return
*/
int save(Map<String, String> userInfo);
String findByPhoneAndPwd(String phone, String pwd);
User findByUserId(Integer userId);
}
@Override
public User findByUserId(Integer userId) {
User user = userMapper.findByUserId(userId);
return user;
}
- DAO
public interface UserMapper {
int save(User user);
User findByPhoneAndPwd(@Param("phone") String phone, @Param("pwd") String pwd);
User findByUserId(@Param("user_id") Integer userId);
}
<?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="net.xdclass.online_xdclass.mapper.UserMapper">
<insert id="save" parameterType="User">
INSERT INTO user (name, pwd, head_img, phone , create_time)
values (#{name,jdbcType=VARCHAR}, #{pwd,jdbcType=VARCHAR}, #{headImg,jdbcType=VARCHAR},
#{phone,jdbcType=VARCHAR},#{createTime,jdbcType=TIMESTAMP})
</insert>
<!--根据手机号和密码找用户-->
<select id="findByPhoneAndPwd" resultType="User">
select * from user where phone =#{phone} and pwd = #{pwd}
</select>
<select id="findByUserId" resultType="User">
select * from user where id=#{user_id}
</select>
</mapper>
- 测试
先登录获取token
Header中传入token
Header中不传token