转自:https://blog.csdn.net/qq_41305266/article/details/80991687、
一、两次MD5
1. 用户端: PASS = MD5( 明文 + 固定 Salt)
2. 服务端: PASS = MD5( 用户输入 + 随机 Salt)
通过两次MD5,可以增大http明文传输过程或数据库被盗后,黑客通过彩虹表等手段反推出明文密码的难度(有一定作用,但不能保证绝对安全)。
pom文件添加依赖
<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>
添加md5加密工具类
package com.wings.seckill.util;
import org.apache.commons.codec.digest.DigestUtils;
public class Md5Util {
private static final String SALT = "1a2b3c4d";
public static String md5(String src){
return DigestUtils.md5Hex(src);
}
public static String inputPass2FormPass(String inputPass){
String src = "" + SALT.charAt(0) + SALT.charAt(2)+ inputPass + SALT.charAt(5)+ SALT.charAt(4);
return md5(src);
}
public static String formPass2DbPass(String formPass, String salt){
String src = "" + salt.charAt(0) + salt.charAt(2)+ formPass + salt.charAt(5)+ salt.charAt(4);
return md5(src);
}
public static String inputPass2DbPass(String inputPass, String salt){
String formPass = inputPass2FormPass(inputPass);
String dbPass = formPass2DbPass(formPass, salt);
return dbPass;
}
public static void main(String[] args) {
String inputPass = "13632481101";
String salt = "mysalt";
String formPass = inputPass2FormPass(inputPass);
String dbPass1 = formPass2DbPass(formPass, salt);
String dbPass2 = inputPass2DbPass(inputPass, salt);
System.out.println(formPass);
System.out.println(dbPass1);
System.out.println(dbPass2);
}
}
新增用户表seckill_user
CREATE TABLE `seckill`.`Untitled` (
`id` bigint(20) NOT NULL COMMENT '用户ID,电话号码',
`nickname` varchar(255) NOT NULL,
`password` varchar(32) DEFAULT null COMMENT 'MD5(MD5(pass明文+固定salt)+salt)',
`salt` varchar(10) DEFAULT NULL,
`head` varchar(128) DEFAULT NULL COMMENT '头像,云存储的id',
`register_date` datetime(0) DEFAULT NULL COMMENT '注册时间',
`last_login_date` datetime(0) COMMENT '上次登录时间',
`login_count` int(11) DEFAULT 0 COMMENT '登录次数',
PRIMARY KEY (`id`)
);
添加电话号码校验工具类
package com.wings.seckill.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.alibaba.druid.util.StringUtils;
public class ValidatorUtil {
private static Pattern MOBILE_PATTERN = Pattern.compile("1\\d{10}");
public static boolean isMobile(String mobile){
if(StringUtils.isEmpty(mobile)){
return false;
}
Matcher matcher = MOBILE_PATTERN.matcher(mobile);
return matcher.matches();
}
public static void main(String[] args) {
boolean result1 = isMobile("13632481101");
boolean result2 = isMobile("1363248110");
System.out.println(result1);
System.out.println(result2);
}
}
SeckillUser domain、dao、service、controller
package com.wings.seckill.domain;
import java.util.Date;
public class SeckillUser {
private Long id;
private String nickname;
private String password;
private String salt;
private String head;
private Date registerDate;
private Date lastLoginDate;
private Integer loginCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public String getHead() {
return head;
}
public void setHead(String head) {
this.head = head;
}
public Date getRegisterDate() {
return registerDate;
}
public void setRegisterDate(Date registerDate) {
this.registerDate = registerDate;
}
public Date getLastLoginDate() {
return lastLoginDate;
}
public void setLastLoginDate(Date lastLoginDate) {
this.lastLoginDate = lastLoginDate;
}
public Integer getLoginCount() {
return loginCount;
}
public void setLoginCount(Integer loginCount) {
this.loginCount = loginCount;
}
}
package com.wings.seckill.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.wings.seckill.domain.SeckillUser;
@Mapper
public interface SeckillUserDao {
@Select("select * from seckill_user where id = #{id}")
public SeckillUser getById(long id);
}
package com.wings.seckill.service;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.wings.seckill.dao.SeckillUserDao;
import com.wings.seckill.domain.SeckillUser;
import com.wings.seckill.result.CodeMsg;
import com.wings.seckill.result.Result;
import com.wings.seckill.util.Md5Util;
import com.wings.seckill.util.ValidatorUtil;
import com.wings.seckill.vo.LoginVo;
@Service
public class SeckillUserService {
@Autowired
SeckillUserDao seckillUserDao;
public SeckillUser getById(long id){
return seckillUserDao.getById(id);
}
public CodeMsg login(LoginVo loginVo){
if(loginVo == null){
return CodeMsg.SERVER_ERROR;
}
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
if(StringUtils.isEmpty(mobile)){
return CodeMsg.MOBILE_EMPTY;
}
if(StringUtils.isEmpty(password)){
return CodeMsg.PASSWORD_EMPTY;
}
if(!ValidatorUtil.isMobile(mobile)){
return CodeMsg.MOBILE_ERROR;
}
SeckillUser user = seckillUserDao.getById(Long.parseLong(loginVo.getMobile()));
if(user == null){
return CodeMsg.MOBILE_NOT_EXIST;
}
String salt = user.getSalt();
String dbPass = user.getPassword();
String md5Pass = Md5Util.formPass2DbPass(password, salt);
if(!dbPass.equals(md5Pass)){
return CodeMsg.PASSWORD_ERROR;
}
return CodeMsg.SUCCESS;
}
}
package com.imooc.miaosha.controller;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.imooc.miaosha.redis.RedisService;
import com.imooc.miaosha.result.Result;
import com.imooc.miaosha.service.MiaoshaUserService;
import com.imooc.miaosha.vo.LoginVo;
@Controller
@RequestMapping("/login")
public class LoginController {
private static Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
MiaoshaUserService userService;
@Autowired
RedisService redisService;
@RequestMapping("/to_login")
public String toLogin() {
return "login";
}
@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(HttpServletResponse response, @Valid LoginVo loginVo) {
log.info(loginVo.toString());
//登录
userService.login(response, loginVo);
return Result.success(true);
}
}
视图层增加如下文件
登陆页面:login.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- jquery -->
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<!-- bootstrap -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}" />
<script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}"></script>
<!-- jquery-validator -->
<script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
<script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
<!-- layer -->
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<!-- md5.js -->
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
<!-- common.js -->
<script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
<form name="loginForm" id="loginForm" method="post" style="width:50%; 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-5">
<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-5">
<input id="password" name="password" class="form-control" type="password" placeholder="密码" required="true" minlength="6" maxlength="16" />
</div>
</div>
</div>
<div class="row">
<div class="col-md-5">
<button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
</div>
<div class="col-md-5">
<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 = g_passsword_salt;
var str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
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("成功");
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>
</html>
全局自定义 common.js
//展示loading
function g_showLoading(){
var idx = layer.msg('处理中...', {icon: 16,shade: [0.5, '#f5f5f5'],scrollbar: false,offset: '0px', time:100000}) ;
return idx;
}
//salt
var g_passsword_salt="1a2b3c4d"
(二)JSR303参数校验 + 全局异常处理器
service中的方法入参有许多参数的判断代码,为了简化,可以利用JSR303参数校验
pom文件引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
自定义参数校验注解
package com.wings.seckill.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.lang3.StringUtils;
import com.wings.seckill.util.ValidatorUtil;
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required;
@Override
public void initialize(IsMobile isMobile) {
required = isMobile.required();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(!required && StringUtils.isEmpty(value)){
return true;
}
return ValidatorUtil.isMobile(value);
}
}
package com.wings.seckill.validator;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = IsMobileValidator.class )
public @interface IsMobile {
boolean required() default true;
String message() default "手机号码格式不正确";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
使用该自定义注解:
package com.wings.seckill.vo;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import com.wings.seckill.validator.IsMobile;
public class LoginVo {
@NotNull
@IsMobile
private String mobile;
@NotNull
@Length(min = 32)
private String password;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
}
}
在对应方法中加入@Valid 开启参数校验
@RequestMapping("/do_login")
@ResponseBody
public Result<CodeMsg> doLogin(@Valid LoginVo loginVo) {
log.info(loginVo.toString());
seckillUserService.login(loginVo);
return Result.success(CodeMsg.SUCCESS);
}
service的login方法简化如下:
public boolean login(LoginVo loginVo){
if(loginVo == null){
//return CodeMsg.SERVER_ERROR;
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
/*
if(StringUtils.isEmpty(mobile)){
return CodeMsg.MOBILE_EMPTY;
}
if(StringUtils.isEmpty(password)){
return CodeMsg.PASSWORD_EMPTY;
}
if(!ValidatorUtil.isMobile(mobile)){
return CodeMsg.MOBILE_ERROR;
}
*/
SeckillUser user = seckillUserDao.getById(Long.parseLong(mobile));
if(user == null){
//return CodeMsg.MOBILE_NOT_EXIST;
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
String salt = user.getSalt();
String dbPass = user.getPassword();
String md5Pass = Md5Util.formPass2DbPass(password, salt);
if(!dbPass.equals(md5Pass)){
//return CodeMsg.PASSWORD_ERROR;
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
return true;
}
定义全局异常及异常处理器
package com.wings.seckill.exception;
import com.wings.seckill.result.CodeMsg;
public class GlobalException extends RuntimeException{
private static final long serialVersionUID = 31665074385012932L;
private CodeMsg cm;
public GlobalException(CodeMsg cm){
this.cm = cm;
}
public CodeMsg getCm() {
return cm;
}
}
package com.wings.seckill.exception;
import javax.servlet.http.HttpServletRequest;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.wings.seckill.result.CodeMsg;
import com.wings.seckill.result.Result;
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public Result<String> handleException(HttpServletRequest request, Exception ex){
ex.printStackTrace();
if(ex instanceof GlobalException){
GlobalException gex = (GlobalException)ex;
return Result.error(gex.getCm());
} else if(ex instanceof BindException){
BindException bex = (BindException)ex;
String message = bex.getAllErrors().get(0).getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillMsg(message));
} else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}
(三)分布式session
添加uuid工具类
package com.wings.seckill.util;
import java.util.UUID;
public class UUIDUtil {
public static String uuid(){
return UUID.randomUUID().toString().replace("-", "");
}
}
SeckillUserService的login方法添加以下代码
String token = UUIDUtil.uuid();
redisService.set(SeckillUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_TOKEN_NAME, token);
cookie.setMaxAge(SeckillUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
SeckillUserService添加通过token获取user对象方法
public SeckillUser getByToke(String token) {
if(StringUtils.isEmpty(token)){
return null;
}
return redisService.get(SeckillUserKey.token, token, SeckillUser.class);
}
登录成功后跳转到商品列表,对应的Controller及页面如下
package com.wings.seckill.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.wings.seckill.domain.SeckillUser;
import com.wings.seckill.service.SeckillUserService;
@Controller
@RequestMapping("/goods")
public class GoodsController {
private static Logger log = LoggerFactory.getLogger(GoodsController.class);
@Autowired
private SeckillUserService seckillUserService;
@RequestMapping("/to_list")
public String toList(Model model,
@CookieValue(name = SeckillUserService.COOKIE_TOKEN_NAME, required = false) String cookieToken,
// @RequestParam 是为了兼容默写手机端会把cookie信息放入请求参数中
@RequestParam(name = SeckillUserService.COOKIE_TOKEN_NAME, required = false) String paramToken) {
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
return "/login/to_login";
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
SeckillUser seckillUser = seckillUserService.getByToke(token);
model.addAttribute("user", seckillUser);
return "goods_list";
}
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>商品列表</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'hello:'+${user.nickname}" ></p>
</body>
</html>
(四)优化
像User这种,几乎每个Controller方法都要使用的对象,如果每个Controller的方法都通过以上方式获取,代码将相当臃肿。这时,可以使用SpringMVC中WebMvcConfigurerAdapter的addArgumentResolvers,向需要user对象的方法进行入参注入。
package com.wings.seckill.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
package com.wings.seckill.config;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.wings.seckill.domain.SeckillUser;
import com.wings.seckill.service.SeckillUserService;
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver{
@Autowired
private SeckillUserService seckillUserService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz == SeckillUser.class;
}
@Override
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(SeckillUserService.COOKIE_TOKEN_NAME);
String cookieToken = getCookieValue(request, SeckillUserService.COOKIE_TOKEN_NAME);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)){
return null;
}
String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
return seckillUserService.getByToke(token, response);
}
private String getCookieValue(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if(cookies != null){
for(Cookie cookie : cookies){
if(cookie.getName().equals(cookieName)){
return cookie.getValue();
}
}
}
return null;
}
}
package com.wings.seckill.service;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.wings.seckill.dao.SeckillUserDao;
import com.wings.seckill.domain.SeckillUser;
import com.wings.seckill.exception.GlobalException;
import com.wings.seckill.redis.RedisService;
import com.wings.seckill.redis.SeckillUserKey;
import com.wings.seckill.result.CodeMsg;
import com.wings.seckill.util.Md5Util;
import com.wings.seckill.util.UUIDUtil;
import com.wings.seckill.vo.LoginVo;
@Service
public class SeckillUserService {
public static final String COOKIE_TOKEN_NAME = "token";
@Autowired
SeckillUserDao seckillUserDao;
@Autowired
RedisService redisService;
public SeckillUser getById(long id){
return seckillUserDao.getById(id);
}
public boolean login(HttpServletResponse response, LoginVo loginVo){
if(loginVo == null){
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String password = loginVo.getPassword();
SeckillUser user = seckillUserDao.getById(Long.parseLong(mobile));
if(user == null){
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
String salt = user.getSalt();
String dbPass = user.getPassword();
String md5Pass = Md5Util.formPass2DbPass(password, salt);
if(!dbPass.equals(md5Pass)){
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
String token = UUIDUtil.uuid();
addCookie(token, response, user);
return true;
}
public SeckillUser getByToke(String token, HttpServletResponse response) {
if(StringUtils.isEmpty(token)){
return null;
}
// 延长有效期
SeckillUser user = redisService.get(SeckillUserKey.token, token, SeckillUser.class);
if(user != null){
addCookie(token, response, user);
}
return user;
}
private void addCookie(String token, HttpServletResponse response, SeckillUser user){
redisService.set(SeckillUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_TOKEN_NAME, token);
cookie.setMaxAge(SeckillUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
}
}
这时Conpackage com.wings.seckill.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.wings.seckill.domain.SeckillUser;
@Controller
@RequestMapping("/goods")
public class GoodsController {
@RequestMapping("/to_list")
public String toList(Model model, SeckillUser seckillUser) {
model.addAttribute("user", seckillUser);
return "goods_list";
}
}