1. 数据库设计
create table miaosha_user (
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 default null comment '注册时间',
last_login_date datetime default null comment '上次登录时间',
login_count int(11) default '0' comment '登录次数',
primary key (id)
)engine=innoDB charset=utf8mb4
2. 明文密码两次MD5处理
导入依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
编写MD5Utils
public class MD5Util {
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
private static final String salt = "hmxP@ssw0rd";
public static String inputPassToFormPass(String inputPass) {
String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
public static String formPassToDBPass(String fromPass, String salt) {
String str = "" + salt.charAt(0) + salt.charAt(2) + fromPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
public static String inputPassToDBPass(String input, String saltDB) {
return formPassToDBPass(inputPassToFormPass(input),saltDB);
}
public static void main(String[] args) {
System.out.println(inputPassToFormPass("hmx"));
System.out.println(formPassToDBPass("hmx", "hmxsfd"));
System.out.println(inputPassToDBPass("hmx","12343sf"));
}
}
2.1. 用户端: PASS = MD5(明文 + 固定Salt)
common.js
function g_showLoading(){
var idx = layer.msg('处理中...', {icon: 16,shade: [0.5, '#f5f5f5'],scrollbar: false,offset: '0px', time:100000}) ;
return idx;
}
var g_passsword_salt="hmxP@ssw0rd"
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" />
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
<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>
<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>
<script type="text/javascript" th:src="@{/layer/layer.js}"></script>
<script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
<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-6">
<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-6">
<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("成功");
window.location.href="/goods/to_list";
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>
</html>
2.2. 服务端: PASS = MD5(用户输入+随机Salt)
ValidatorUtil.java
public class ValidatorUtil {
public static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
public static boolean isMobile(String mobile) {
if (StringUtils.isBlank(mobile)) {
return false;
}
Matcher m = mobile_pattern.matcher(mobile);
return m.matches();
}
public static void main(String[] args) {
System.out.println(isMobile("18936095619"));
}
}
MiaoshaUser.java
@Data
public class MiaoshaUser {
private Long id;
private String nickname;
private String password;
private String salt;
private String head;
private Date registerDate;
private Date lastLoginDate;
private Integer loginCount;
}
MiaoshaUserMapper.java
public interface MiaoshaUserMapper extends BaseMapper<MiaoshaUser> {
}
MiaoshaUserService.java
@Service
public class MiaoshaUserService {
@Autowired
private MiaoshaUserMapper miaoshaUserMapper;
public Result<MiaoshaUser> findById(Long userId) {
return Result.success(miaoshaUserMapper.selectById(userId));
}
public Result<CodeMsg> login(LoginVo loginVo) {
if (loginVo == null) {
return Result.fail(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
MiaoshaUser user = miaoshaUserMapper.selectById(mobile);
if (user == null) {
return Result.fail(CodeMsg.MOBILE_NOT_EXIST);
}
String dbPass = user.getPassword();
String dbSalt = user.getSalt();
String pwd = MD5Util.formPassToDBPass(formPass, dbSalt);
if (!StringUtils.equals(pwd, dbPass)) {
return Result.fail(CodeMsg.PASSWORD_ERROR);
}
return Result.success(null);
}
}
LoginController.java
@Controller
@RequestMapping("/login")
public class LoginController {
private static final Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
private MiaoshaUserService miaoshaUserService;
@RequestMapping("/to_login")
public String toLogin() {
return "login";
}
@RequestMapping("/do_login")
@ResponseBody
public Result<CodeMsg> doLogin(LoginVo loginVo) {
log.info(loginVo.toString());
String passForm = loginVo.getPassword();
String mobile = loginVo.getMobile();
if (StringUtils.isBlank(passForm)) {
return Result.fail(CodeMsg.PASSWORD_BLANK);
}
if (StringUtils.isBlank(mobile)) {
return Result.fail(CodeMsg.MOBILE_BLANK);
}
if (!ValidatorUtil.isMobile(mobile)) {
return Result.fail(CodeMsg.MOBILE_ERROR);
}
return miaoshaUserService.login(loginVo);
}
}
新增CodeMsg
SESSION_ERROR(500210, "Session不存在或者已经失效"),
PASSWORD_BLANK(500211, "登录密码不能为空"),
MOBILE_BLANK(500212, "手机号不能为空"),
MOBILE_ERROR(500213, "手机号格式错误"),
MOBILE_NOT_EXIST(500214, "手机号不存在"),
PASSWORD_ERROR(500215, "密码错误");
3. JSR303参数校验+全局异常处理器
JSR303参数校验
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.5.2</version>
</dependency>
新增CodeMsg
BIND_ERROR(500101,"参数校验异常:%s")
IsMobile.java
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {IsMobileValidator.class}
)
public @interface IsMobile {
boolean required() default true;
String message() default "手机号码格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
IsMobileValidator.java
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false;
@Override
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
int i = 1;
if (required) {
return ValidatorUtil.isMobile(s);
}
if (StringUtils.isBlank(s)) {
return true;
}
return ValidatorUtil.isMobile(s);
}
}
LoginVo.java
@Data
public class LoginVo {
@NotNull
@IsMobile
private String mobile;
@NotNull
@Length(min=32)
private String password;
}
GlobalException.java
public class GlobalException extends RuntimeException{
private static final long serialVersionUID = 1L;
private CodeMsg codeMsg;
public GlobalException(CodeMsg cm) {
this.codeMsg = cm;
}
public CodeMsg getCodeMsg() {
return codeMsg;
}
}
GlobalExceptionHandler.java
@ControllerAdvice
@ResponseBody
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.fail(ex.getCodeMsg());
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
List<ObjectError> erros = ex.getAllErrors();
ObjectError error = erros.get(0);
String msg = error.getDefaultMessage();
return Result.fail(CodeMsg.BIND_ERROR.fillArgs(msg));
}
return Result.fail(CodeMsg.SERVER_ERROR);
}
}
MiaoshaUserService.java
@Service
public class MiaoshaUserService {
@Autowired
private MiaoshaUserMapper miaoshaUserMapper;
public Result<CodeMsg> login(LoginVo loginVo) {
if (loginVo == null) {
int i = 1;
throw new GlobalException(CodeMsg.SERVER_ERROR) ;
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
MiaoshaUser user = miaoshaUserMapper.selectById(mobile);
if (user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
String dbPass = user.getPassword();
String dbSalt = user.getSalt();
String pwd = MD5Util.formPassToDBPass(formPass, dbSalt);
if (!StringUtils.equals(pwd, dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
return Result.success(null);
}
}
4. 通过redis实现分布式Session
生成token
判断用户的登录信息正确之后,我们生成一个随机的token,生成一个Cookie返回给浏览器,并保存一份到redis中 MiaoshaUserService.java
public static final String COOKIE_NAME_TOKEN = "token";
public MiaoshaUser getByToken(String token) {
if (StringUtils.isBlank(token)) {
return null;
}
MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
if (user == null) {
throw new GlobalException(CodeMsg.TOKEN_ERROR);
}
return user;
}
public Result<CodeMsg> login(HttpServletResponse response, LoginVo loginVo) {
if (loginVo == null) {
int i = 1;
throw new GlobalException(CodeMsg.SERVER_ERROR) ;
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
MiaoshaUser user = miaoshaUserMapper.selectById(mobile);
if (user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
String dbPass = user.getPassword();
String dbSalt = user.getSalt();
String pwd = MD5Util.formPassToDBPass(formPass, dbSalt);
if (!StringUtils.equals(pwd, dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
String token = UUIDUtil.uuid();
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN,token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
return Result.success(null);
}
GoodController.java
@Controller
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private MiaoshaUserService miaoshaUserService;
@RequestMapping("/to_list")
public String toList(Model model,
@CookieValue(value = MiaoshaUserService.COOKIE_NAME_TOKEN, required = false) String cookieToken,
@RequestParam(value = MiaoshaUserService.COOKIE_NAME_TOKEN, required = false) String paramToken) {
if (StringUtils.isBlank(cookieToken) && StringUtils.isBlank(paramToken)) {
return "login";
}
String token = StringUtils.isBlank(paramToken) ? cookieToken : paramToken;
MiaoshaUser user = miaoshaUserService.getByToken(token);
model.addAttribute("user", user);
return "goods_list";
}
}
CodeMsg
TOKEN_ERROR(500216, "token错误");
MiaoshaUserKey.java
public class MiaoshaUserKey extends BasePrefix{
public static final int TOKEN_EXPIRE = 3600 * 24;
private MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "token");
}
完善并优化
WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserArgumentResolver userArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userArgumentResolver);
}
}
UserArgumentResolver.java
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private MiaoshaUserService miaoshaUserService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz == MiaoshaUser.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(MiaoshaUserService.COOKIE_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if (StringUtils.isBlank(cookieToken) && StringUtils.isBlank(paramToken)) {
return null;
}
String token = StringUtils.isBlank(paramToken) ? cookieToken : paramToken;
return miaoshaUserService.getByToken(response, token);
}
private String getCookieValue(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
return null;
}
}
GoodsController.java
@Controller
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private MiaoshaUserService miaoshaUserService;
@RequestMapping("/to_list")
public String toList(Model model, MiaoshaUser user) {
model.addAttribute("user", user);
return "goods_list";
}
}
|