秒杀系统 - 实现用户登录(两次MD5,JSR303参数检验,全局异常处理器)和分布式session功能
作者:互联网
文章目录
用户登录
数据库设计
CREATE TABLE `user` (
`id` bigint(20) NOT NULL COMMENT '用户ID,手机号码',
`nickname` varchar(255) NOT NULL COMMENT '昵称',
`password` varchar(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt) + salt)',
`salt` varchar(10) DEFAULT NULL COMMENT '第二次加密的salt',
`head` varchar(128) DEFAULT NULL COMMENT '头像,云存储的ID',
`register_date` datetime DEFAULT NULL COMMENT '注册时间',
`last_login_date` datetime DEFAULT NULL COMMENT '上次登录时间',
`login_count` int(11) DEFAULT '0' COMMENT '登录次数',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
明文密码两次MD5处理
加密思路
http在网络是以明文来传输的,数据包可能被劫持,是不安全的。
第一次加密是客户端发送请求之前, 采用的是固定salt,MD5(密码+固定salt)
,加密后发送请求传给服务端(加盐混淆密码,MD5明文转为密文)。
第二次加密是写到数据库之前,要先生成一个随机salt,MD5(密码+随机salt)
,把随机salt
和加密结果同时存数据库。
安全性
两次加密增加了破解难度,并不是无法破解的。
如果想更安全,可以采用https,浏览器插件ActiveX(网银的那些安全控件),控件破解难度高,相对来说更安全。二通过js是无法做到数据安全的,以为js本身都是明文。
加密过程
导入MD5依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>
封装MD5Utils
MD5Utils类用于加密。
public class MD5Util {
//固定salt
private static final String salt = "1d2c3b4a";
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
//第一次加密
public static String inputToForm(String input){
String str = salt.charAt(0) + salt.charAt(2) + input + salt.charAt(4) + salt.charAt(5);
return(md5(str));
}
//第二次加密
public static String formToDB(String form,String salt){
String str = ""+salt.charAt(0)+salt.charAt(2)+form + salt.charAt(3) + salt.charAt(5);
return md5(str);
}
//两次加密
public static String inputToDB(String input,String salt){
String formPass = inputToForm(input);
return formToDB( formPass,salt );
}
}
测试代码
public static void main(String[] args){
String i = "214143";
System.out.println(md5(i));
System.out.println(inputToForm(i));
System.out.println(formToDB( inputToForm(i) ,"12345678"));
System.out.println( inputToDB(i,"12345678") );
}
测试结果
实现登录页面
bootstrap画页面。
jquery-validation做form表单验证。
layer做弹框。
md5.js做md5加密。
直接用教程的演示代码了。
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
略
</head>
<body>
<form name="loginForm" id="loginForm" method="post" style="width:30%; margin:0 auto;">
<h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入手机号码</label>
<div class="col-md-8">
<input id="mobile" name = "mobile" class="form-control" type="text" placeholder="手机号码" required="true" minlength="11" maxlength="11" />
</div>
<div class="col-md-1">
</div>
</div>
</div>
<div class="form-group">
<div class="row">
<label class="form-label col-md-4">请输入密码</label>
<div class="col-md-8">
<input id="password" name="password" class="form-control" type="password" placeholder="密码" required="true" minlength="6" maxlength="16" />
</div>
</div>
</div>
<div class="row" style="margin-top:40px;">
<div class="col-md-6">
<button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
</div>
<div class="col-md-6">
<button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
</div>
</div>
</form>
</body>
<script>
function login(){
$("#loginForm").validate({
submitHandler:function(form){
doLogin();
}
});
}
function doLogin(){
g_showLoading();
var inputPass = $("#password").val();
var salt = "1d2c3b4a";
var str = salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(4) + salt.charAt(5);
var password = md5(str);
$.ajax({
url: "/login/do_login",
type: "POST",
data:{
mobile:$("#mobile").val(),
password: password
},
success:function(data){
layer.closeAll();
if(data.code == 0){
layer.msg("成功");
window.location.href="/goods/to_list";
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>
</html>
Controller加loginController类,这样浏览器访问 http://localhost:8080/login/to_login 就可以看到登录页。
@Controller
@RequestMapping("/login")
public class LoginController {
@RequestMapping("/to_login")
public String toLogin() {
return "login";
}
}
JSR303参数检验
简介
前端传过来的字段如何在后台做效验,最老的方法就是if else,但显得不是很灵活。如果前端传来100个字段就得写许多多余的代码。
可以在后台创建的实体和前端传来的字段做对应映射,加上JSR303注解来做灵活的效验。
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
流程
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用注解
验证信息的常用注解主要有:
@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Past 验证注解的元素值(日期类型)比当前时间早
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
自定义注解
我们可以自定义isMobile注解,检查是否符合手机号的格式。
IsMobile 注解:
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) //适用范围:方法,遍历,注解,构造方法,方法参数
@Retention(RUNTIME) //运行期间保留
@Documented //文档显示注解
@Constraint(validatedBy = {IsMobileValidator.class }) //通过自定义IsMobileValidator类注解约束
public @interface IsMobile {
boolean required() default true;
String message() default "手机号码格式错误"; // 约束注解验证时的输出消息
Class<?>[] groups() default { }; // 约束注解在验证时所属的组别
Class<? extends Payload>[] payload() default { };// 约束注解的有效负载
}
IsMobile 注解关联的验证器IsMobileValidator类,继承了ConstraintValidator接口,需要指定两个参数,第一个自定义注解类,第二个为需要校验的数据类型。
实现接口后要override两个方法,分别为initialize方法和isValid方法。方法 initialize 对验证器进行实例化,它必须在验证器的实例在使用之前被调用,并保证正确初始化验证器,isValid方法就是我们最终需要的校验方法了。
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}
public boolean isValid(String value, ConstraintValidatorContext context) {
if(required) {
return ValidatorUtil.isMobile(value);
}else {
if(StringUtils.isEmpty(value)) {
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
}
}
ValidatorUtil类,用来检查是否符合手机号格式:
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}"); //表示1开头,并且后面跟10个数字。
public static boolean isMobile(String src) {
if(StringUtils.isEmpty(src)) {
return false;
}
Matcher m = mobile_pattern.matcher(src);
return m.matches();
}
}
现在只要加上@Valid
注解,就可以吧参数校验部分删除了。
在登陆的时候,会抛出BindException的异常,可以在控制台看到,但是doLogin方法没办法把Result.error(CodeMsg)返回,页面也就看不到“密码错误”的报错信息了。
测试
请求参数前加注解@Valid,表示我们对这个对象属性需要进行验证
在LoginVo类中加注解(这里爆红是应为截图的时候还没写好IsMobile注解)。
全局异处理器
原因
最常见的异常处理方式,就是使用try{}catch。一个Controller下面,满屏幕的try{}catch,看着一点都不优雅,所以可以对所有异常实施统一处理的方案。
流程
GlobalException类
自定义全局异常类,数据格式不规范会被我们手动抛出。
serialVersionUID
public class GlobalException extends RuntimeException{
private static final long serialVersionUID = 1L; //序列化id
private CodeMsg codeMsg;
public GlobalException(CodeMsg codeMsg) {
super(codeMsg.toString());
this.codeMsg= codeMsg;
}
public CodeMsg getCodeMsg () {
return codeMsg;
}
}
GlobalExceptionHandler类
用来捕获,处理异常。
@ControllerAdvice //表示实现:全局异常处理,全局数据绑定,全局数据预处理
@ResponseBody //java对象转为json格式的数据
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class) //所有异常都拦截
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {
GlobalException ex = (GlobalException)e; //全局异常
return Result.error(ex.getCm());
}else if(e instanceof BindException) { //绑定异常
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors(); //参数校验可能有很多错误,这里只返回第一个。
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
}else {
return Result.error(CodeMsg.SERVER_ERROR);//其他异常一律视为服务器错误
}
}
}
接口
有了全局异常处理器,接口就可以写的更清晰了,直接执行userService.login函数,如果有问题直接抛出异常,然后被全局异常处理器捕获,并进一步处理。
接口:
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(@Valid LoginVo loginVo) {
userService.login(loginVo);
return Result.success(true);
}
userService.login函数:
public void login(LoginVo loginVo){
if(loginVo == null){
throw new GlobalException( CodeMsg.SERVER_ERROR );
}
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();``
MiaoshaUser miaoshaUser = getById(Long.parseLong(mobile ));
if( miaoshaUser == null ){
throw new GlobalException( CodeMsg.MOBILE_NOT_EXIST );
}
//验证密码
String dbPass = miaoshaUser.getPassword();
String salt = miaoshaUser.getSalt();
String pass = MD5Util.formToDB(password,salt);
if(pass.equals(dbPass)){
throw new GlobalException( CodeMsg.SUCCESS );
}else{
throw new GlobalException( CodeMsg.PASSWORD_ERROR );
}
}
测试
可以看到异常已经处理好了。
分布式session
session和 cookie
- 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制(Session)来识具体的用户。 典型的场景比如购物车,当你点击下单按钮时,并不知道是哪个用户操作的,所以服务端要为特定的用户创建特定的Session,用用于标识这个用户。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
- 每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。如果客户端的浏览器禁用了 Cookie,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
- Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。
简单来说,用户登陆之后,服务端给用户生成一个sessionid(token)来标识用户,写到cookie中传递客户端,客户端在随后访问中在cookie中上传token,服务端拿到token从而辨别出唯一的用户。
流程
写login接口
Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。
request和response对象即然代表请求和响应,那我们要获取客户机提交过来的数据,只需要找request对象就行了。要向客户机输出数据,只需要找response对象就行了。
登录的时候因为要给客户机发送session信息,所以多传一个参数HttpServletResponse 。
//跳转到login页面
@RequestMapping("/to_login")
public String toLogin() {
return "login";
}
//登录接口
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response,@Valid LoginVo loginVo) {
miaoshaUserService.login(response ,loginVo);
return Result.success(true);
}
miaoshaUserService类
getById(): 通过id从mysql中查询用户信息
getByToken(): 通过token从redis中查询用户信息,并且把生成的Cookie放入HttpServletResponse 。
login(): 参数校验,如果密码不对就抛异常,会被全局异常捕获识别。
addCookie(): redis中存入键值对(session, user实体),同时给HttpServletResponse 赋值session信息,存活时间,根目录。
@Service
public class MiaoshaUserService {
@Autowired
MiaoshaUserDao miaoshaUserDao;
@Autowired
RedisService redisService;
public static final String COOKI_TOKEN_NAME = "name";
public MiaoshaUser getById(Long id){
return miaoshaUserDao.getById(id);
}
public MiaoshaUser getByToken(HttpServletResponse response,String token){
if(StringUtils.isEmpty(token)){
return null;
}
MiaoshaUser user = redisService.get(MiaoshaUserKey.token,token,MiaoshaUser.class);
//延长有效期
if(user != null){
addCookie(response,user);
}
return user;
}
public boolean login(HttpServletResponse response, LoginVo loginVo){
if(loginVo == null){
throw new GlobalException( CodeMsg.SERVER_ERROR );
}
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
MiaoshaUser miaoshaUser = getById(Long.parseLong(mobile ));
if( miaoshaUser == null ){
throw new GlobalException( CodeMsg.MOBILE_NOT_EXIST );
}
//验证密码
String dbPass = miaoshaUser.getPassword();
String salt = miaoshaUser.getSalt();
String pass = MD5Util.formToDB(password,salt);
if(!pass.equals(dbPass)){
throw new GlobalException( CodeMsg.PASSWORD_ERROR );
}
//生成cookie
addCookie(response,miaoshaUser);
return true;
}
private void addCookie(HttpServletResponse response,MiaoshaUser miaoshaUser){
String token = UUIDUtil.uuid();
redisService.set(MiaoshaUserKey.token,token,miaoshaUser);
Cookie cookie = new Cookie(COOKI_TOKEN_NAME,token);
cookie.setMaxAge(MiaoshaUserKey.token.getExpireSeconds());
cookie.setPath("/"); //设置根目录
response.addCookie(cookie);
}
}
登录成功跳转到list
ajax请求成功之后,会跳转到/goods/to_list接口。
$.ajax({
url: "/login/do_login",
type: "POST",
data:{
mobile:$("#mobile").val(),
password: password
},
success:function(data){
layer.closeAll();
if(data.code == 0){
layer.msg("成功");
window.location.href="/goods/to_list";
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
通过session信息查用户
对应的/goods/to_list接口:
@RequestMapping("/to_list")
public String toList(HttpServletResponse response,Model model,
@CookieValue(value = MiaoshaUserService.COOKI_TOKEN_NAME,required = false) String cookieToken,
@RequestParam(value = MiaoshaUserService.COOKI_TOKEN_NAME,required = false) String paramToken
){
if(StringUtils.isEmpty(cookieToken)&& StringUtils.isEmpty(paramToken))
return "login";
String token = StringUtils.isEmpty(paramToken)? cookieToken:paramToken;//paramToken优先
MiaoshaUser miaoshaUser = userService.getByToken(response,token); //根据token信息查出miaoshaUser详细信息
model.addAttribute("user",miaoshaUser);
return "goods_list";
}
但是这种写法比较复杂,如果其他部分也需要通过session查到miaoshaUser,就会重复写很多类似的语句。
新增HandlerMethodArgumentResolver
改进写法如下,也就是省略HttpServletResponse,String cookieToken, String paramToken 这三个参数。多了MiaoshaUser 这个参数。
改进步骤1:
WebConfig.java addArgumentResolvers是给controller方法的参数赋值的。上文的toList方法有参数MiaoshaUser,遍历参数,如果有MiaoshaUser类,我们就通过addArgumentResolvers方法给他赋值。
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
改进步骤2:
UserArgumentResolver类实现HandlerMethodArgumentResolver 接口,重写两个方法supportsParameter resolveArgument
。supportsParameter用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。而resolveArgument就可以放我们之前toList()方法中的逻辑。
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
MiaoshaUserService userService;
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz== MiaoshaUser.class;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class );
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
String paramToken = request.getParameter(MiaoshaUserService.COOKI_TOKEN_NAME);
String cookieToken = getCookieValue(request,MiaoshaUserService.COOKI_TOKEN_NAME);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookiTokenName) {
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
if(cookie.getName().equals(cookiTokenName)){
return cookie.getValue();
}
}
return null;
}
}
改进完成,现在代码就精简许多了。
@RequestMapping("/to_list")
public String toList(Model model,MiaoshaUser miaoshaUser
){
model.addAttribute("user",miaoshaUser);
return "goods_list";
}
测试结果
goods_list.html如下:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
略
</head>
<body>
<p th:text = "'nickname,'+${user.nickname}"></p>
<p th:text = "'id,'+${user.id}"></p>
<p th:text = "'password,'+${user.password}"></p>
<p th:text = "'salt,'+${user.salt}"></p>
<p th:text = "'head,'+${user.head}"></p>
<p th:text = "'registerDate,'+${user.registerDate}"></p>
<p th:text = "'lastLoginDate,'+${user.lastLoginDate}"></p>
<p th:text = "'loginCount,'+${user.loginCount}"></p>
</body>
</html>
可以看到已经通过session读取到数据库中的用户信息
标签:return,String,JSR303,参数检验,session,注解,login,salt,public 来源: https://blog.csdn.net/weixin_44532671/article/details/117368762