1 分布式session 1.1 用户登录功能实现 分布式Session 异常处理器 根据token获取用户信息 1 .1.1 使用两次MD5 1 2 3 4 5 1.用户端:PASS = MD5(明文+固定Salt) 防止用户明文密码在网络中传输 2.服务端:PASS = MD5(用户输入+随机Salt) 防止被脱裤 引入MD5工具类,添加MD5Util
1 2 3 4 5 6 7 8 9 10 11 <!--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>
1 .1.2 MD5Util 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.imooc.miaosha.util;import org.apache.commons.codec.digest.DigestUtils;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" )); } }
1.1.3 使用 JSR303参数校验 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
先在参数中加上注解
1 doLogin(@Valid LoginVo loginVo)
在实体类中校验
1 2 3 4 5 6 @NotNull private String mobile;@NotNull @Length (min=32 )private String password;
1.1.4 如何自定义一个验证器,比如检验手机号格式 先定义校验注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.imooc.miaosha.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 { }; }
1.1.5 再定义一个真正校验的 IsMobileValidator.class
要实现 ConstraintValidator 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.imooc.miaosha.validator;import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;import org.apache.commons.lang3.StringUtils;import com.imooc.miaosha.util.ValidatorUtil;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); } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.imooc.miaosha.util;import java.util.regex.Matcher;import java.util.regex.Pattern;import org.apache.commons.lang3.StringUtils;public class ValidatorUtil { private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}" ); public static boolean isMobile (String src) { if (StringUtils.isEmpty(src)) { return false ; } Matcher m = mobile_pattern.matcher(src); return m.matches(); } }
现在这样会有异常,定义一个全局异常拦截
拦截所有异常,如果是绑定异常就返回对应的错误 如果是其他异常就返回服务端错误
2.1.1 异常处理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.imooc.miaosha.exception;import java.util.List;import javax.servlet.http.HttpServletRequest;import org.springframework.validation.BindException;import org.springframework.validation.ObjectError;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import com.imooc.miaosha.result.CodeMsg;import com.imooc.miaosha.result.Result;@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.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); } } }
之后我们就可以抛出异常然后让其捕获就可以了
我们创建一个全局异常,然后抛出,可以在上面捕获在进行指定输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.imooc.miaosha.exception;import com.imooc.miaosha.result.CodeMsg;public class GlobalException extends RuntimeException { private static final long serialVersionUID = 1L ; private CodeMsg cm; public GlobalException (CodeMsg cm) { super (cm.toString()); this .cm = cm; } public CodeMsg getCm () { return cm; } }
3 分布式Session 登录成功 给这个用户生成一个UUID,也就是token,传递给客户端。 客户端每次都上传这个 token就能获取到用户信息了。
这个token对应哪个 用户信息存到 redis中
将信息存到第三方缓存中
cookie增加成功,添加成功 token
在登录完成的最后一步,需要带着Session信息。
1.利用uuid生成秘钥(sessionId)
2.将user信息,对象。同时写入cookie cookie作为response返回给客户端, 另外 将sessionId +前缀 一起作为Key,存入Redis 缓存中。当访问其他页面的时候,就可以从cookie中获取 token,再访问redis 拿到用户信息来判断登录情况了。
MiaoshaUserKey类: 1 2 3 4 5 6 7 8 9 10 11 12 13 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" ); }
WebConfig类 1 2 3 4 5 6 7 8 9 10 11 @Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Autowired UserArgumentResolver userArgumentResolver; @Override public void addArgumentResolvers (List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(userArgumentResolver); } }
UserArgumentResolver类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Service public class UserArgumentResolver implements HandlerMethodArgumentResolver { @Autowired MiaoshaUserService userService; @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.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 ; } }
Service层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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; } private void addCookie (HttpServletResponse response,MiaoshaUser miaoshaUser) { String token = UUIDUtil.uuid(); redisService.set(MiaoshaUserKey.token,token,miaoshaUser); Cookie cookie = new Cookie(COOKIE_NAME_TOKEN,token); cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds()); cookie.setPath("/" ); response.addCookie(cookie); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RequestMapping ("/to_list" )public String toLogin (Model model, @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false ) String cookieToken, @RequestParam (value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false ) String paramToken, HttpServletResponse response) { if (StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){ return "do_login" ; } String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken; MiaoshaUser user =userService.getByToken(response,token); model.addAttribute("user" ,user); return "to_list" ; }
1 2 3 4 5 6 7 8 9 private void addCookie (HttpServletResponse response, String token, MiaoshaUser user) { redisService.set(MiaoshaUserKey.token, token, user); Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token); cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds()); cookie.setPath("/" ); response.addCookie(cookie); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public boolean login (HttpServletResponse response, LoginVo loginVo) { if (loginVo == null ) { throw new GlobleException(CodeMsg.SERVER_ERROR); } String mobile = loginVo.getMobile(); String formPass = loginVo.getPassword(); MiaoshaUser user = getByMobile(mobile); if (user == null ) { throw new GlobleException(CodeMsg.MOBILE_NOT_EXIST); } String dbPass = user.getPassword(); String saltDB = user.getSalt(); String calcPass = MD5Util.formPassToDBPass(formPass, saltDB); if (!calcPass.equals(dbPass)) { throw new GlobleException(CodeMsg.PASSWORD_ERROR); } String token = UUIDUtil.uuid(); addCookie(response, token, user); return true ; }
spring-boot整合spring-session,使用redis共享
https://www.jianshu.com/p/cdf327a6a5a4
spring-boot整合spring-session,使用redis共享 12018.05.13 00:50:57字数 703阅读 10688
本文讲述spring-boot工程中使用spring-session机制进行安全认证,并且通过redis存储session,满足集群部署、分布式系统的session共享。
java工程中,说到权限管理和安全认证,我们首先想到的是Spring Security和Apache Shiro,这两者均能实现用户身份认证和复杂的权限管理功能。但是如果我们只是想实现身份认证(如是否登录、会话是否超时),使用session管理即可满足。本文目录如下:
目录: 1. 创建spring-boot项目 2. 用户管理 3. 用户身份认证 4. spring-session配置 5. 使用redis共享session
一、创建spring-boot项目 1、工程使用idea+gradle搭建,jdk1.8,spring-boot版本2.0.2.RELEASE,数据库postgreSQL,持久层spring-data-jpa; 2、新建spring-boot项目,工程type选择Gradle Project; 3、勾选初始化依赖如下:
初始化依赖
创建完成后gradle.build文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 buildscript { ext { springBootVersion = '2.0.2.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion} " ) } } apply plugin: 'java' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'louie.share' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.boot:spring-boot-starter-data-jpa' ) compile('org.springframework.boot:spring-boot-starter-web' ) runtime('org.springframework.boot:spring-boot-devtools' ) runtime('org.postgresql:postgresql' ) testCompile('org.springframework.boot:spring-boot-starter-test' ) }
4、application.yml配置数据库及jpa
1 2 3 4 5 6 7 8 9 10 spring: datasource: driver-class -name : org.postgresql.Driver url: jdbc:postgresql: data-username: louie password: louie1234 jpa: database: postgresql hibernate: ddl-auto : update
二、用户管理 1、创建User实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import org.hibernate.annotations.GenericGenerator;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.Table;import java.io.Serializable;@Entity @Table (name = "b_id_user" )public class User implements Serializable { @Id @GenericGenerator (name = "idGenerator" ,strategy = "uuid" ) @GeneratedValue (generator = "idGenerator" ) private String id; @NotBlank (message = "account can not be empty" ) private String account; @NotBlank (message = "password can not be empty" ) private String password; @NotBlank (message = "name can not be empty" ) private String name; }
2、用户服务接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import louie.share.sessionredis.bean.User;public interface UserService { User saveUser (User user) ; User findByAccount (String account) ; BaseResponse login (String account,String password) ; }
这里省略接口的实现,您可以访问我的github和码云查看该工程的源代码(代码地址见文档底部)。
3、用户管理控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import louie.share.sessionredis.bean.BaseResponse;import louie.share.sessionredis.bean.User;import louie.share.sessionredis.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping (value = "/user" )public class UserController { @Autowired private UserService userService; @RequestMapping (value = "/save" ,method = RequestMethod.POST) public User save (User user) { return userService.saveUser(user); } @RequestMapping (value = "/find/{account}" ,method = RequestMethod.GET) public User find (@PathVariable String account) { return userService.findByAccount(account); } @RequestMapping (value = "/login" ,method = RequestMethod.POST) public BaseResponse login (String account,String password) { return userService.login(account,password); } }
4、创建用户 运行Application类启动服务,使用postMan访问http://localhost:8080/user/save 服务创建用户:
创建用户
三、用户身份认证 1、使用postMan访问http://localhost:8080/user/login 进行用户登录校验:
微信截图_20180512184322.png-66.2kB
四、spring-session配置 该部分为重点内容了,目的是实现访问资源时的安全认证、超时控制和用户登出功能。 1、修改用户登录login控制,登录成功后将用户信息写入session
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RequestMapping (value = "/login" ,method = RequestMethod.POST) public BaseResponse login (String account, String password,HttpSession session) { BaseResponse response = userService.login(account,password); if (response.isOk()){ session.setAttribute(session.getId(),response.getData()); } return response; }
2、新增用户登出logout功能,将用户信息移除session
1 2 3 4 5 6 7 8 9 10 @RequestMapping (value = "/logout" )public String logout (HttpSession session) { session.removeAttribute(session.getId()); return "user logout success" ; }
3、设置session过期时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: datasource: driver-class -name : org.postgresql.Driver url: jdbc:postgresql: data-username: louie password: louie1234 jpa: database: postgresql hibernate: ddl-auto : update server: servlet: session: timeout: "PT10M"
以下是为session有效时长为10分钟:
设置session过期时间
4、添加拦截器,通过session判断用户是否有效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import com.alibaba.fastjson.JSON;import louie.share.sessionredis.bean.BaseResponse;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import java.io.IOException;@Configuration public class SessionCofig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new SecurityInterceptor()) .excludePathPatterns("/user/login" ) .excludePathPatterns("/user/logout" ) .addPathPatterns("/**" ); } @Configuration public class SecurityInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { HttpSession session = request.getSession(); if (session.getAttribute(session.getId()) != null ){ return true ; } response.getWriter().write(JSON.toJSONString( new BaseResponse(){{ setOk(false ); setMessage("please login first" ); }} )); return false ; } } }
5、使用postMan访问http://localhost:8080/user/find/101 ,用户未登录,被拦截:
用户未登录拦截
访问http://localhost:8080/user/login 登录:
用户登录
再次访问访问http://localhost:8080/user/find/101 :
登录后访问
五、使用redis存储session 1、添加依赖
1 2 compile('org.springframework.boot:spring-boot-starter-data-redis' ) compile('org.springframework.session:spring-session-data-redis' )
2、application.yml中添加配置
redis配置
源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 spring: datasource: driver-class -name : org.postgresql.Driver url: jdbc:postgresql: data-username: louie password: louie1234 jpa: database: postgresql hibernate: ddl-auto : update redis: database: 0 host: localhost port: 6379 password: xonro_vflow session: store-type: redis server: servlet: session: timeout: "PT10M"
3、启动redis和application类,用户登录,查看redis内容:
debug查看:
debug查看session信息
redis内容:
redis内容
工程代码已共享至github和码云,欢迎探讨学习。
Author:
John Doe
Permalink:
http://yoursite.com/2019/01/30/项目实战/java秒杀系统方案优化/
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
Do you believe in DESTINY?