主要内容
- 数据库设计
- 明文密码两次MD5处理
- JSR303参数检验+全局异常处理
- 分布式Session
一、数据库设计
二、两次MD5
两次MD5的原因:
- HTTP在网络上是明文传输的,为了防止不法分子截取数据包,那么就可能获得我们的登陆密码
两次MD5的设计方案
- 用户端:PASS = MD5(明文+固定salt)
- 服务端:PASS = MD5(用户输入+随机salt)
导包
//用于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>
封装一个MD5加密工具类
public class MD5Util {
public static String md5(String src) {
return DigestUtils.md5Hex(src);
}
private static final String salt = "1a2b3c4d";
public static String inputPassToFormPass(String inputPass) {
String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
System.out.println(str);
return md5(str);
}
public static String formPassToDBPass(String formPass, String salt) {
String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);
return md5(str);
}
public static String inputPassToDbPass(String inputPass, String saltDB) {
String formPass = inputPassToFormPass(inputPass);
String dbPass = formPassToDBPass(formPass, saltDB);
return dbPass;
}
public static void main(String[] args) {
System.out.println(inputPassToFormPass("123456"));//d3b1294a61a07da9b49b6e22b2cbd7f9
System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d"));
System.out.println(inputPassToDbPass("123456", "1a2b3c4d"));//b7797cce01b4b131b433b6acf4add449
}
}
三、登陆功能的实现
登录页面的展现
1.LoginController
@RequestMapping("/to_login")
public String toLogin() {
return "login";
}
2.处理页面
完成登陆功能
1.完成上图中login()方法
<script>
function login(){
/*参数校验,验证通过会回调doLogin方法,可以翻阅相关jquery-validator的内容*/
$("#loginForm").validate({
submitHandler:function(form){
doLogin();
}
});
}
function doLogin(){
//展示转圈
g_showLoading();
//第一次MD5
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();
//layer.msg(data.msg);
if(data.code == 0){
layer.msg("成功");
}else{
layer.msg(data.msg);
}
},
error:function(){
layer.closeAll();
}
});
}
</script>
2.为方便接收参数新建LoginVo
public class LoginVo {
private String mobile;
private String password;
3.controller包中的LoginController增加方法
@RequestMapping("/do_login")
@ResponseBody
public Result doLogin(LoginVo loginVo) {
//输出到idea控制台
log.info(loginVo.toString());
//参数校验
String passInput = loginVo.getPassword();
String mobile = loginVo.getMobile();
//密码是否为空
if(StringUtils.isEmpty(passInput)){
return Result.error(CodeMsg.PASSWORD_EMPTY);
}
//手机号是否为空
if(StringUtils.isEmpty(mobile)){
return Result.error(CodeMsg.MOBILE_EMPTY);
}
//手机号格式见4
if(ValidatorUtil.isMobile(mobile)){
return Result.error(CodeMsg.MOBILE_ERROR);
}
//登录见5
CodeMsg cm = userService.login(loginVo);
return Result.success(true);
}
4.为了验证手机号格式写一个ValidatorUtil类
/**
* 手机号验证
*/
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("^[1](([3|5|8][\\d])|([4][4,5,6,7,8,9])|([6][2,5,6,7])|([7][^9])|([9][1,8,9]))[\\d]{8}$");
public static boolean isMobile(String src){
if(StringUtils.isEmpty(src)){
return false;
}
Matcher matcher = mobile_pattern.matcher(src);
// System.out.println(matcher.matches());
return matcher.matches();
}
public static void main(String[] args) {
System.out.println(isMobile("110"));
System.out.println(isMobile("13626527917"));
}
}
5.服务层MiaoshaUserService
@Service
public class MiaoshaUserService {
@Autowired
MiaoshaUserDao miaoshaUserDao;
public MiaoshaUser getById(long id) {
return miaoshaUserDao.getByid(id);
}
public CodeMsg login(LoginVo loginVo) {
if(loginVo == null) {
return CodeMsg.SESSION_ERROR;
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在见7
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null) {
return CodeMsg.MOBILE_NOT_EXIST;
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
return CodeMsg.PASSWORD_ERROR;
}
return CodeMsg.SUCCESS;
}
}
6.domain包中MiaoshaUser
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;
7.数据层MiaoshaUserDao
@Repository
public interface MiaoshaUserDao {
@Select("select * from miaosha_user where id=#{id}")
public MiaoshaUser getByid(@Param("id") long id);
@Update("update miaosha_user set password=#{password} where id = #{id}")
public void update(MiaoshaUser toBeUpdate);
}
至此初步登陆功能实现了
四、JSR303参数校验
使用原因:如下图这么写就很烦
JSR303很强大可以找文档学习
导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
使用注解
1.@Validated注解
2.处理LoginVo类
3.实现自定义注解
IsMobile
//复制别的注解的
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@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 { };
}
isMobileValidator
//<IsMobile, String> 校验谁,注解修饰什么类型
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 value, ConstraintValidatorContext context) {
if(required){
return ValidatorUtil.isMobile(value);
}else{
//如果不是必须的,且value为空可以返回true
if(StringUtils.isEmpty(value)){
return true;
}else{
//不为空验证格式
return ValidatorUtil.isMobile(value);
}
}
}
}
显示错误信息
使用注解后如果没有通过校验会在控制台报错,把错误信息输出可以先拦截异常然后根据异常信息输出异常
如下参数校验异常时:
1.GlobalExceptionHandler
但是这么写不好啊,可以设置成就拦截GlobalException和BindException
牛客那个项目返回json都转成字符串到js中再转成对象,这里直接返回对象
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e) {
//见2
if (e instanceof GlobalException) {
GlobalException ex = (GlobalException) e;
return Result.error(ex.getCodeMsg());
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
//见3
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
} else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}
2.GlobalException
public class GlobalException extends RuntimeException {
private CodeMsg codeMsg;
public GlobalException(CodeMsg codeMsg){
super();
this.codeMsg = codeMsg;
}
public CodeMsg getCodeMsg() {
return codeMsg;
}
}
3.CodeMsg增加如下代码
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
public CodeMsg fillArgs(Object... args) {
int code = this.code;
String message = String.format(this.msg, args);
return new CodeMsg(code, message);
}
4.修改MiaoshaUserService
遇到问题就抛出去就行了
public boolean login(LoginVo loginVo) {
if(loginVo == null) {
throw new GlobalException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user = getById(Long.parseLong(mobile));
if(user == null) {
throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
throw new GlobalException(CodeMsg.PASSWORD_ERROR);
}
return true;
}
4.修改MiaoshaUserService
@RequestMapping(path="/do_login",method = RequestMethod.POST)
@ResponseBody
public Result doLogin(@Validated LoginVo loginVo) {
//输出到idea控制台
log.info(loginVo.toString());
//登录
userService.login(loginVo);
return Result.success(true);
}
五、分布式Session
把一个客户端传来的token映射一个User
- 生成一个名为token的Cookie传给客户端
- 客户端每次访问时携带此token
- 服务端根据此token识别用户
生成Cookie
1.写一个生成uuid的工具类
public class UUIDUtil {
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
2.修改MiaoshaUserService增加添加Cookie的代码
public boolean login(HttpServletResponse response,LoginVo loginVo) {
。。。忽略代码
//生成cookie
String token = UUIDUtil.uuid();
addCookie(response, token, user);
return true;
}
public static final String COOKI_NAME_TOKEN = "token";
private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
//见3
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
}
3.MiaoshaUserKey
public class MiaoshaUserKey extends BasePrefix{
public static final int TOKEN_EXPIRE = 3600*24 * 2;
private MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}
public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
}
从Cookie中读取token转换成User
1.MiaoshaUserService中增加getByToken方法
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, token, user);
}
return user;
}
2.为了方便使用User避免多次重复写从redis中根据token获取对象的操作,写一个配置类
作用是当读取到controller中方法参数包含MiaoshaUser时自动注入。
3.新建config.WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
//见4
@Autowired
UserArgumentResolver userArgumentResolver;
//这个方法用于管理能往controller中参数中注入值的对象
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}
4.UserArgumentResolver
@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 {
//获取request和response
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
//从参数中取值和从cookie中取值,为了兼容不同情况
String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
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 cookiName) {
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}
5.使用一波