1、使用 Redis 用户登录分析
2、使用工具类生成验证码
- 将随机生成的验证码存放到 redis
- 使用 for循环随机生成,使用StringBuilder保存4个字符串
- 使用 HttpServletResponse 将图片响应到浏览器
VerifyCodeController
package com.czxy.controller;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @author 庭前云落
* @Date 2020/3/26 8:58
* @description
*/
@RestController
@RequestMapping("/verifycode")
public class VerifyCodeController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping
public void verifyCode(String username, HttpServletResponse response) throws IOException {
//字体只显示大写,去掉1,0,i,o几个容易混淆的字符
String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
int IMG_WIDTH = 72;
int IMG_HEIGTH = 27;
Random random = new Random();
//创建图片
BufferedImage image = new BufferedImage(IMG_WIDTH, IMG_HEIGTH, BufferedImage.TYPE_INT_RGB);
//画板
Graphics g = image.getGraphics();
//填充背景
g.setColor(Color.WHITE);
g.fillRect(1, 1, IMG_WIDTH - 2, IMG_HEIGTH - 2);
//设置字体
g.setFont(new Font("楷体", Font.BOLD, 25));
// 使用 StringBuilder 保存字符串
StringBuilder stringBuilder = new StringBuilder();
//绘制4个字符
for (int i = 1; i <= 4; i++) {
//随机颜色
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
//随机生成4个字符
int len = random.nextInt(VERIFY_CODES.length());
String str = VERIFY_CODES.substring(len, len + 1);
//存放字符串
stringBuilder.append(str);
g.drawString(str, IMG_WIDTH / 6 * i, 22);
}
//将验证码存放到redis
stringRedisTemplate.opsForValue().set("login" + username, stringBuilder.toString(), 5, TimeUnit.MINUTES);
//生成随机干扰线
for (int i = 0; i < 30; i++) {
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
int x = random.nextInt(IMG_WIDTH - 1);
int y = random.nextInt(IMG_HEIGTH - 1);
int x1 = random.nextInt(12) + 1;
int y1 = random.nextInt(6) + 1;
g.drawLine(x, y, x - x1, y - y1);
//响应到浏览器
ImageIO.write(image, "jpeg", response.getOutputStream());
}
}
}
3、用户登录整合 JWT
3.1、生产 Token
-
用户登录成功,生成token,并将token响应到浏览器。
-
1、yml文件里,添加jwt配置信息
sc: jwt: secret: sc@Login(Auth}*^31)&czxy% # 登录校验的密钥 pubKeyPath: D:/rsa/rsa.pub # 公钥地址 priKeyPath: D:/rsa/rsa.pri # 私钥地址 expire: 360 # 过期时间,单位分钟
-
2、创建 JwtProperties文件,用于加载 sc.jwt 配置信息
package com.czxy.config;
import com.czxy.utils.RasUtils;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
* @author 庭前云落
* @Date 2020/3/29 13:29
* @description
*/
@Data
@ConfigurationProperties(prefix = "sc.jwt")
@Component
public class JwtProperties {
private String secret; // 密钥
private String pubKeyPath;// 公钥
private String priKeyPath;// 私钥
private int expire;// token过期时间
private PublicKey publicKey; // 公钥
private PrivateKey privateKey; // 私钥
private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
@PostConstruct
public void init() {
try {
File pubFile = new File(this.pubKeyPath);
File priFile = new File(this.priKeyPath);
if (!pubFile.exists() || !priFile.exists()) {
RasUtils.generateKey(this.pubKeyPath, this.priKeyPath, this.secret);
}
this.publicKey = RasUtils.getPublicKey(this.pubKeyPath);
this.privateKey = RasUtils.getPrivateKey(this.priKeyPath);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 3、注入 JwtProperties,并使用 JwtUtils 生成 token
Controller
package com.czxy.controller;
import com.czxy.config.JwtProperties;
import com.czxy.pojo.User;
import com.czxy.service.AuthService;
import com.czxy.utils.JwtUtils;
import com.czxy.vo.BaseResult;
import org.springframework.data.redis.core.StringRedisTemplate;
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.annotation.Resource;
/**
* @author 庭前云落
* @Date 2020/3/27 8:44
* @description
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
@Resource
private AuthService authService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private JwtProperties jwtProperties;
@PostMapping("/login")
public BaseResult login(@RequestBody User user) throws Exception {
//验证码校验
String code = stringRedisTemplate.opsForValue().get("login" + user.getUsername());
if(code==null){
return BaseResult.error("验证码无效");
}
if(!code.equalsIgnoreCase(user.getCode())){
return BaseResult.error("验证码错误");
}
User login = authService.login(user);
if(login!=null){
//生成Token
String token = JwtUtils.generateToken(login,jwtProperties.getExpire(),jwtProperties.getPrivateKey());
//登录成功删除验证码
stringRedisTemplate.delete("login"+user.getUsername());
return BaseResult.ok("登录成功").append("login",login).append("token",token);
}else{
return BaseResult.error("用户名或密码不匹配");
}
}
}
AuthService
package com.czxy.service;
import com.czxy.feign.UserFeign;
import com.czxy.pojo.User;
import com.czxy.utils.BCrypt;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author 庭前云落
* @Date 2020/3/27 8:41
* @description
*/
@Service
public class AuthService {
@Resource
private UserFeign userFeign;
public User login(User user){
User result = userFeign.findByUsername(user);
if(result==null){
return null;
}
//校验密码
boolean checkpw = BCrypt.checkpw(user.getPassword(), result.getPassword());
if(checkpw){
return result;
}
return null;
}
}
3.2、Token 校验
-
在项目网关完成 token 的校验
-
1、网关 yml 文件 添加 jwt 配置,同时配置白名单
白名单:不需要拦截的资源在yml中进行配置,在过滤器直接放行
#新增 sc: jwt: secret: sc@Login(Auth}*^31)&czxy% # 登录校验的密钥 pubKeyPath: D:/rsa/rsa.pub # 公钥地址 priKeyPath: D:/rsa/rsa.pri # 私钥地址 expire: 360 # 过期时间,单位分钟 filter: allowPaths: - /checkusername - /checkmobile - /sms - /register - /login - /verifycode - /categorys - /news - /brands - /specifications - /search - /goods - /comments
-
2、将 auth_service 里的 JwtProperties 复制到网关配置,同时创建白名单配置文件(存放允许放行的路径)
FilterProperties
package com.czxy.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author 庭前云落
* @Date 2020/3/29 15:17
* @description
*/
@Data
@ConfigurationProperties(prefix = "sc.filter")
@Component
public class FilterProperties {
//允许访问路径集合
private List<String> allowPaths;
}
- 3、编写网关过滤器,对所有路径进行拦截
package com.czxy.filter;
import com.czxy.config.FilterProperties;
import com.czxy.config.JwtProperties;
import com.czxy.pojo.User;
import com.czxy.utils.JwtUtils;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* @author 庭前云落
* @Date 2020/3/29 14:51
* @description
*/
@Component
//2.1 加载JWT配置类
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
public class LoginFilter extends ZuulFilter {
//2.2 注入jwt配置类实例
@Resource
private JwtProperties jwtProperties;
@Resource
private FilterProperties filterProperties;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 5;
}
@Override
public boolean shouldFilter() { // 3 当前过滤器是否执行,true执行,false不执行
//1、获得工具类(请求上下文)
RequestContext currentContext = RequestContext.getCurrentContext();
//2、获得请求对象
HttpServletRequest request = currentContext.getRequest();
//3、获得请求路径 v3/authservice/login
String requestURI = request.getRequestURI();
//3.2 如果路径是 /authservice/login 当前拦截器不执行
for (String path : filterProperties.getAllowPaths()) {
//判断包含
if (requestURI.contains(path)) {
return false;
}
}
//执行run()方法
return true;
}
@Override
public Object run() throws ZuulException {
//1、获得token
//1.1 获得上下文
RequestContext currentContext = RequestContext.getCurrentContext();
//1.2 获得request对象
HttpServletRequest request = currentContext.getRequest();
//1.3 获得指定请求头的值
String token = request.getHeader("Authorization");
//2 校验 token --- 使用 JWT工具类进行解析
//2.3 使用工具类 通过公钥获得对应信息
try {
JwtUtils.getObjectFromToken(token, jwtProperties.getPublicKey(), User.class);
} catch (Exception e) {
//2.4 如果有异常 -- 没有登录(没有权限)
currentContext.addOriginResponseHeader("content-type", "text/html;charset=UTF-8");
currentContext.addZuulResponseHeader("content-type", "text/html;charset=UTF-8");
currentContext.setResponseStatusCode(403);
currentContext.setResponseBody("token失效或无效");
currentContext.setSendZuulResponse(false);
}
//如果没有异常 放行
return null;
}
}
4、前端
绑定事件,切换验证码
4.1、使用 Token
- 1、登录成功后保存token(
login.vue
)
<template>
<!-- 省略 -->
</template>
<script>
export default {
data() {
return {
imgSrc: "",
errorMsg: "",
user: {
username: "",
password: ""
}
};
},
methods: {
changeVerifyCode() {
//判断必须要输入用户名
if (this.user.username) {
//切换图片路径
this.imgSrc = `http://localhost:10010/v3/cgwebservice/verifycode?t=${new Date()}&username=${
this.user.username
}`;
} else {
this.errorMsg = "用户名不能为空";
}
},
async login() {
let { data } = await this.$request.login(this.user);
if (data.code == 1) {
//成功
sessionStorage.setItem("user", JSON.stringify(data.other.login));
//保存token
sessionStorage.setItem("token", data.other.token);
//this.$router.push("/"); 为了首页的asyncData数据获取,选择重定向
location.href='/'
} else {
this.erroMsg = data.message;
}
}
},
watch: {
user: {
handler(v) {
if (v) {
//如果user数据发生改变,修改提示信息
this.errorMsg = "";
}
},
deep: true
}
}
};
</script>
-
2、请求自动携带 token,即将 token 添加到请求头(
plugins/api.js
) -
同时添加处理 403 异常的方法
var axios = null export default ({ $axios }, inject) => { //参考 https://axios.nuxtjs.org/helpers let token = sessionStorage.getItem('token') if (token) { $axios.setToken(token) } //处理响应异常 $axios.onError(error => { //token失效 服务器响应403 if (error.response.status === 403) { console.error(error.response.data) redirect('/login') } }) //3) 保存内置的axios axios = $axios //4) 将自定义函数交于nuxt // 使用方式1:在vue中,this.$request.xxx() // 使用方式2:在nuxt的asyncData中,content.app.$request.xxx() inject('request', request) }
-
3、修改 nuxt.conf.js,将 api.js 插件模式 改成"client"
-
client:仅前端客户端有效,默认:客户端服务端都执行
- 否则抛异常
sessionStorage is not defined
- 否则抛异常