-
手机验证码+Redis登录验证+token+登录拦截
2021-04-13 20:06:33手机验证码+Redis登录验证+token+登录拦截 文章目录手机验证码+Redis登录验证+token+登录拦截解决方案思想以阿里云为例1.阿里云官网开通短信服务2.创建签名3.创建短信模板4.依赖短信服务的包5.编写发短信的工具类6....手机验证码+Redis登录验证+token+登录拦截
文章目录
解决方案
- 各种云平台
- 各种数据商
- 各种第三方的延伸产品
思想
- 用户使用手机验证码登录
- 前端点击按钮通过第三方云平台发送短信验证码
- 在发送验证码前先判断用户是否存在
- 如果用户存在则发送验证码,同时把验证码存入redis中,key为手机号value为随机验证码
- 用户收到验证码后点击登录进行登录验证
- 通过前端传的手机号在redis中查询数据,判断验证码是否匹配
- 如果匹配则登录成功,同时清除redis中的验证码数据
- 此时生成一串uuid
- 首先将uuid作为key,用户信息作为value存入redis中,以免前端保存用户信息不安全
- 再把uuid作为参数生成token传到前端,然后再把token保存至浏览器中
- 定义一个拦截器
- 前端每次请求时都在请求头中携带token信息
- 后端定义过滤器,判断请求头中携带token是否正确
- 如果判断错误则抛出异常,定义异常处理类返回状态码
- 状态码如果未登录则在前端跳转至登录页面,如果登录状态则正常访问
以阿里云为例
1.阿里云官网开通短信服务
2.创建签名
3.创建短信模板
4.依赖短信服务的包
<dependency> <groupId>com.aliyun</groupId> <artifactId>dysmsapi20170525</artifactId> <version>2.0.1</version> </dependency>
5.编写发短信的工具类
public class SMSUtils { //参数一表示接收短信的手机号 //参数二表示发送的验证码 public static void sendSms(String phone, String code) { try { //加载外部配置文件 InputStream resourceAsStream = SMSUtils.class.getClassLoader().getResourceAsStream("sms.properties"); Properties properties = new Properties(); properties.load(resourceAsStream); //初始化发送人 com.aliyun.dysmsapi20170525.Client client = createClient(properties.getProperty("accessKeyId"), properties.getProperty("accessKeySecret")); SendSmsRequest sendSmsRequest = new SendSmsRequest() //要发送的手机号 .setPhoneNumbers(phone) //签名 .setSignName("xxxx") //模板编号 .setTemplateCode("SMS_173425292") //发送的验证码 .setTemplateParam("{\"code\":" + code + "}"); //发送短信验证码 client.sendSms(sendSmsRequest); } catch (Exception e) { e.printStackTrace(); } } public static com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception { Config config = new Config() // 您的AccessKey ID .setAccessKeyId(accessKeyId) // 您的AccessKey Secret .setAccessKeySecret(accessKeySecret); // 访问的域名 config.endpoint = "dysmsapi.aliyuncs.com"; return new com.aliyun.dysmsapi20170525.Client(config); } }
6.前端发送请求携带手机号到后端
7.开启Redis
- 导入jedis的依赖
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
- 编写redis工具类
public class RedisUtils { private static JedisPool jedisPool; //初始化连接池 static { InputStream resourceAsStream = RedisUtils.class.getClassLoader().getResourceAsStream("redis.properties"); Properties properties = new Properties(); try { properties.load(resourceAsStream); } catch (IOException e) { e.printStackTrace(); } //初始化连接池的各种属性 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(Integer.parseInt(properties.getProperty("maxTotal"))); jedisPoolConfig.setMaxIdle(Integer.parseInt(properties.getProperty("maxIdle"))); jedisPool = new JedisPool(jedisPoolConfig, properties.getProperty("host"), Integer.parseInt(properties.getProperty("port"))); } //从连接池中取出一个jedis public static Jedis getJedis(){ return jedisPool.getResource(); } //存值得方法 public static void setCache(String key,String value,String redisKey){ Jedis jedis = getJedis(); //存入redis中值 jedis.set(redisKey+key,value); //把连接放回连接池 jedis.close(); } //取值的方法 public static String getCache(String key,String redisKey){ Jedis jedis = getJedis(); //以key取redis中的值 String s = jedis.get(redisKey+key); jedis.close(); //返回取出的值 return s; } }
8.后端接收数据
@GetMapping("getCode") public AxiosResult<Void> getCode(String phone){ //通过手机号查询数据库验证用户是否存在 User userByPhone = userService.findUserByPhone(phone); if (userByPhone!=null){ //如果数据存在则生成6位随机数字验证码 int code =(int) (Math.random()*(999999-100000+1)+100000); //先把验证码存入redis中,key为前端传入的手机号,value为随机数字验证码 RedisUtils.setCache(phone,code+"",RedisKey.PREFIX_KEY); //调用工具类发送短信验证码 SMSUtils.sendSms(phone,code+""); return AxiosResult.success(); } return AxiosResult.error(); }
9.token的使用
- 引入依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
- 编写token工具类
@Component public class TokenUtils { //自定义秘钥 private static final String SECRET = "0x6c717a"; //生成token public String getToken(String uuid) { try { //参数为自定义秘钥 Algorithm algorithm = Algorithm.HMAC256(SECRET); String token = JWT.create() //签名 .withIssuer("阿飞") //token中携带的值 .withClaim("uuid", uuid) .sign(algorithm); return token; } catch (JWTCreationException exception) { //Invalid Signing configuration / Couldn't convert Claims. } return ""; } //解析token public String verifyToken(String token){ try { //秘钥 Algorithm algorithm = Algorithm.HMAC256(SECRET); JWTVerifier verifier = JWT.require(algorithm) //签名 .withIssuer("阿飞") .build(); //Reusable verifier instance //解析token DecodedJWT jwt = verifier.verify(token); //通过生成token是存入参数的key取出 Claim uuid = jwt.getClaim("uuid"); //返回从token中解析出的值 return uuid.asString(); } catch (JWTVerificationException exception){ System.out.println(exception.toString()); } return ""; } }
10.使用工具类做到对象与字符串之间的转换
- 依赖jackson的包
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <scope>provided</scope> </dependency>
- 创建工具类
public class JsonUtils { private static ObjectMapper objectMapper = new ObjectMapper(); //对象转字符串方法 public static String Obj2Str(Object o) { try { String s = objectMapper.writeValueAsString(o); return s; } catch (JsonProcessingException e) { e.printStackTrace(); } return ""; } //字符串转对象方法 public static <T>T Str2Obj( String str, Class<T> clazz){ try { T t = objectMapper.readValue(str, clazz); return t; } catch (IOException e) { e.printStackTrace(); } return null; } }
11.前端用户收到验证码后发送登录请求
@PostMapping("doLogin") public AxiosResult<String> doLogin(String phone,String code){ //通过前端传来的手机号从redis中取值 String cache = RedisUtils.getCache(phone, RedisKey.PREFIX_KEY); //判断redis中的值与前端传来的验证码是否一致 if(code.equals(cache)){ //通过手机号查询数据库查出用户信息 User user = userService.findUserByPhone(phone); //生成一个uuid String uuid = UUID.randomUUID().toString(); //把用户信息转成字符串存入redis中 RedisUtils.setCache(uuid, JSONUtils.obj2str(user),RedisKey.LOGIN_USER); //传入uuid生成token返回前端 String token = TokenUtils.getToken(uuid); return AxiosResult.success(token); } return AxiosResult.error(); }
12.每次请求都携带token
axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 config.baseURL = 'http://localhost:8080/'; //添加请求头Authorization: Bearer <token>格式 config.headers.Authorization="Bearer "+localStorage.getItem("token"); return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); });
13.定义后端拦截器
- 编写java代码
//创建一个类实现HandlerInterceptor接口 public class LoginInterceptor implements HandlerInterceptor { @Override //重写本方法 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取请求头中的数据 String authorization = request.getHeader("Authorization"); //判断token是否合法,如果不合法则抛出自定义异常,合法则返回true继续执行代码 if(StringUtils.isEmpty(authorization)){ throw new MyException(EnumStatus.NO_LOGIN); } if(!authorization.startsWith("Bearer ")){ throw new MyException(EnumStatus.NO_LOGIN); } String[] s = authorization.split(" "); if (s[1].equals("null")){ throw new MyException(EnumStatus.NO_LOGIN); } String s1 = s[1]; DecodedJWT decodedJWT = TokenUtils.verifierToken(s1); if (decodedJWT==null){ throw new MyException(EnumStatus.NO_LOGIN); } return true; } }
- 配置拦截器
配置文件的方式
<!-- 在spring的xml配置文件中配置拦截器--> <mvc:interceptors> <mvc:interceptor> <!--要拦截的请求路径--> <mvc:mapping path="/**"/> <!--放行的请求,一般登录请求和获取验证码的请求要放行--> <mvc:exclude-mapping path="/doLogin"/> <mvc:exclude-mapping path="/getCode"/> <!--配置拦截器的路径--> <bean class="com.qy28.sm.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>
注解的方式
@Configuration @ComponentScan(basePackages = {"com.qy28.cn"}, excludeFilters = { @ComponentScan.Filter(Service.class) }) @EnableWebMvc public class ControllerConfig implements WebMvcConfigurer { //使用@Autowired要先把拦截器放入容器中,如果不在容器中可以直接new @Autowired private LoginInterceptor loginInterceptor; @Override //重写本方法配置拦截器 public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).addPathPatterns("/**") .excludePathPatterns("/common/doLogin","/common/verifyCode","/common/getActivateEmail","/employee/doActivate"); } }
14.定义异常处理类
//类上添加@RestControllerAdvice注解 @RestControllerAdvice public class MyExceptionHandler { //方法上添加@ExceptionHandler注解参数为 自定义异常.class @ExceptionHandler(HttpException.class) public AxiosResult<Void> myHandler(MyException e) { //捕捉到异常后返回前端状态码 return AxiosResult.error(e.getEnumStatus()); } }
15.前端验证状态码
axios.interceptors.response.use(function (response) { // 对响应数据做点什 let {status, data, massage} = response.data; if (status == 200) { return data; } else if(status==305){ //如果未登录则跳转至登录页面 location.replace(`../login.html`) }else { if(status==303){ $("#activate").css("display","block"); } $(document).Toasts('create', { body: massage, title: '登录失败', icon: 'fas fa-envelope fa-lg', }) } return Promise.reject(false); }, function (error) { // 对响应错误做点什么 return Promise.reject(error); });
16.用户数据
如果之后的请求逻辑需要用到用户信息的话,直接从请求头中获取token,然后通过工具类解析token,从reids中取用户信息,可以减少与数据库的交互,也保证了用户信息的安全
//抽取成一个方法 public static User getUser(){ //通过请求从请求头中获取token String token = ServletUtils.getRequest().getHeader("Authorization").split(" ")[1]; //解析token获取redis中的key String uuid = verifierToken(token).getClaim("UUID").asString(); //通过key获取数据 String cache = RedisUtils.getCache(uuid, RedisKey.LOGIN_USER); //字符串转对象 User user = JSONUtils.str2Obj(cache, User.class); return user; }
-
事实上在此我可以负责的告诉你,在sa-token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权! 当你受够Shiro、Security等框架的...
-
事实上在此我可以负责的告诉你,在sa-token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权! 当你受够Shiro、Security等框架的...
-
token替代session进行登录验证
2020-08-11 09:37:43文章目录LoginHandler(将登陆成功的token存入响应头发给前端)前端axios请求与响应拦截器写法LoginInterceptor(spring注册登录拦截器)CurrentLoginUser... * 通过手机号和验证码验证登录 * * @return */ @Post文章目录
LoginHandler(将登陆成功的token存入响应头发给前端)
@Controller @RequestMapping("login") public class LoginHandler { @Resource private LoginService loginService; /** * 获取登录验证码 * * @param phone 手机号 * @return */ @GetMapping("valCode") public ResponseEntity getValidateCode(String phone) { String valCode = loginService.getValCode(phone); return ResponseEntity.ok(new ResponseBean(StatusEnum.OPE_SUC, valCode)); } /** * 通过手机号和验证码验证登录 * * @return */ @PostMapping("doLogin") public ResponseEntity doLogin(@RequestBody Map<String, String> map, HttpSession session) { String phone = map.get("phone"); String valCode = map.get("valCode"); Myuser myuser = loginService.doLogin(phone, valCode); if (myuser == null) { throw new MyExceptin(StatusEnum.OPE_ERR); } // session.setAttribute("LOGIN_USER", myuser); // return ResponseEntity.ok(new ResponseBean(StatusEnum.LOGIN_SUC, myuser)); // 登陆成功,生成token字符串 Map<String, Object> token = new HashMap<>(); token.put("phone", phone); String generate = JwtUtil.generate(token); // 将token信息放在响应头中发给客户端 myuser.setUpass(""); HttpHeaders headers = new HttpHeaders(); headers.set("token", generate); return new ResponseEntity(new ResponseBean(StatusEnum.LOGIN_SUC, myuser), headers, HttpStatus.OK); } // 注销操作 @GetMapping("doLogout") public ResponseEntity diLogout(HttpServletRequest request) { String token = request.getHeader("token"); loginService.doLogout(token); return ResponseEntity.ok(new StatusBean(StatusEnum.OPE_SUC)); } }
LoginServiceImpl(登录与注销操作与redis交互)
@Service public class LoginServiceImpl implements LoginService { private Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class); @Resource private ShardedJedisPool jedisPool; @Resource private LoginMapper loginMapper; /** * 获取验证码 * * @param phone 手机号 * @return */ @Override public String getValCode(String phone) { if (phone == null) { throw new MyExceptin(StatusEnum.PHONE_EMPTY); } String valCode = StringRandom.getValCode(); // 打印日志 logger.info("获取到了验证码,验证码为:" + valCode); ShardedJedis resource = jedisPool.getResource(); // 5分钟自动过期 resource.setex(RedisHeadInfo.CODE_HEAD + phone, 30 * 60, valCode); resource.close(); return valCode; } /** * 验证登录信息,返回用户信息 * * @param phone 登录手机号 * @param valCode 登录验证码 * @return */ @Override public Myuser doLogin(String phone, String valCode) { if (phone == null) { throw new MyExceptin(StatusEnum.PHONE_EMPTY); } ShardedJedis resource = jedisPool.getResource(); // 查看登录黑名单是否有此对象,有即删除 if (resource.hexists(RedisHeadInfo.LOGOUT_KEY, phone)) { logger.info("黑名单中有此用户数据,此次操作为重新登陆,将此用户信息移除黑名单"); resource.hdel(RedisHeadInfo.LOGOUT_KEY, phone); }; resource.close(); // 验证码验证 boolean checked = checkValidateCode(phone, valCode); // 验证不通过 if (!checked) { throw new MyExceptin(StatusEnum.OPE_ERR); } // 验证通过 return getMyuserByPhone(phone); } /** * 进行验证码校验 * * @param phone * @param valCode * @return */ private boolean checkValidateCode(String phone, String valCode) { ShardedJedis resource = jedisPool.getResource(); String code = resource.get(RedisHeadInfo.CODE_HEAD + phone); if (code == null || valCode == null || !code.equals(valCode.trim())) { logger.info("验证码为空或验证码输入错误,操作账户为:" + phone + ",正确验证码为:" + code + ",用户输入的验证码为:" + valCode); resource.close(); return false; } resource.close(); return true; } /** * 检测是否是已注册用户。 * 是的话,合法账户登录。执行登录逻辑 * 否的话,注册新用户,并加标志表示这是一个新用户,执行登录逻辑,返回 * * @param phone * @return */ public Myuser getMyuserByPhone(String phone) { ShardedJedis resource = jedisPool.getResource(); // 查看黑名单中是否有此用户数据,有即说明该用户已登出,此次访问为恶意访问 if (resource.hexists(RedisHeadInfo.LOGOUT_KEY, phone)){ resource.close(); logger.info("用户已登出,存在非法访问"); throw new MyExceptin(StatusEnum.USER_LOGOUT); } // 先查缓存中是否存有用户信息 String userStr = resource.hget(RedisHeadInfo.LOGIN_KEY, phone); if (userStr != null) { logger.info("缓存中存有该用户信息"); resource.close(); return JSON.parseObject(userStr, Myuser.class); } // 缓存中没有用户信息,去数据库查找 Myuser myuser = loginMapper.selectByPhone(phone); if (myuser == null) { // 说明是新用户,新插入一条数据,以默认值自动注册 logger.info("数据库无此用户信息,自动注册"); myuser = new Myuser(); myuser.setUname("游客"); myuser.setUpass("1234"); myuser.setUphone(phone); loginMapper.insertMyuser(myuser); myuser.setNewuser("yes"); // 非数据库字段 } // 将用户的登录信息存入redis resource.hset(RedisHeadInfo.LOGIN_KEY, phone, JSON.toJSONString(myuser)); logger.info("将用户的登录信息存入了redis"); resource.close(); return myuser; } /** * 注销操作 * * @param token * @return */ @Override public void doLogout(String token) { Claims claim = JwtUtil.getClaim(token); Object phone = claim.get("phone"); ShardedJedis resource = jedisPool.getResource(); resource.hset(RedisHeadInfo.LOGOUT_KEY, (String) phone, "1"); logger.info("将注销用户的信息存入了redis黑名单"); resource.close(); } }
前端axios请求与响应拦截器写法
axios.defaults.baseURL = 'http://localhost:8080/0807/'; // axios.defaults.withCredentials = true; //跨域配置 //配置发送请求前的拦截器 可以设置token信息 axios.interceptors.request.use( config => { // debugger; let token = localStorage.getItem("token"); let requestUrl = config.url; if (requestUrl != "login/doLogin" && requestUrl.indexOf("login/valCode") != 0) { if (token != null) { // 向请求头加入token信息 config.headers.token = token; }else{ // 未登录,转到登录页面 location.href = "http://127.0.0.1:8848/ssm/login.html"; } } return config }, error => { return Promise.reject(error) } ) // 配置响应拦截器 axios.interceptors.response.use( res => { // if(res.data.code == 50001){ // // 说明用户未登录 // location.href = "http://127.0.0.1:8848/ssm/login.html"; // } // debugger; if(res.data.data == 50002 || res.data.data == 50003){ // token过期失效或者用户已登出的非法访问 location.href = "http://127.0.0.1:8848/ssm/login.html"; } // 将登陆成功后的token存入localSorage if ("token" in res.headers) { // let token = res.headers.token; localStorage.setItem("token", token); } return Promise.resolve(res.data) // 这里直接返回data, 即接口返回的所有数据 }, error => { return Promise.reject(error); } )
前端页面登出
islogout() { this.$confirm('此操作将退出系统, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { axios.get('login/doLogout') .then(response => { console.log(response); if (response.code == 20000) { localStorage.removeItem("token"); location.href = "http://127.0.0.1:8848/ssm/login.html"; } }).catch(function(error) { console.log(error); }); // location.href = "/secondStage/login?method=logout"; }); }
LoginInterceptor(spring注册登录拦截器)
跨域设置开启暴露响应头设置 <mvc:cors> <mvc:mapping path="/**" exposed-headers="token" allow-credentials="true"/> </mvc:cors> 配置登陆拦截器 <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login/doLogin"></mvc:exclude-mapping> <mvc:exclude-mapping path="/login/valCode"></mvc:exclude-mapping> <mvc:exclude-mapping path="/login/doLogout"></mvc:exclude-mapping> <bean class="com.javasm.common.interceptor.LoginInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>
public class LoginInterceptor implements HandlerInterceptor { @Autowired private CurrentLoginUser loginUser; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 对预检请求放行 String method = request.getMethod(); if (method.equals("OPTIONS")) { return true; } String token = request.getHeader("token"); if (token != null) { Claims claim = JwtUtil.getClaim(token); if(claim!=null){ // 可以设置当前登录用户信息,共享至其他接口 String phone = (String)claim.get("phone"); loginUser.setLoginUser(phone); //刷新token String s = refreshToken(claim); if(s!=null){ response.addHeader("token",s); } return true; }else{ //token已过期 throw new MyExceptin(StatusEnum.TOKEN_EXPIR); } } else { throw new MyExceptin(StatusEnum.OPE_ERR); } } private String refreshToken(Claims claim) { Date nowDate = new Date(); Date expiration = claim.getExpiration(); //如果剩余有效时间仅剩3分钟的话,刷新token if ((expiration.getTime() - nowDate.getTime()) <= 3 * 60 * 1000) { //要刷新token Object phone = claim.get("phone"); Map<String, Object> map = new HashMap<>(); map.put("phone", phone); String token = JwtUtil.generate(map); return token; } return null; } }
CurrentLoginUser(共享当前登录用户信息)
- 将登录用户信息放入threadLocal维护,使得其他接口也能获得当前登录的用户信息
@Component public class CurrentLoginUser { @Resource private LoginService loginService; private static final ThreadLocal<Myuser> MYUSER_THREAD_LOCAL = new ThreadLocal<>(); // 通过手机号查询已登录用户信息 public Myuser getLoginUser(){ return MYUSER_THREAD_LOCAL.get(); } public void setLoginUser(String phone){ Myuser loginUser = loginService.getMyuserByPhone(phone); MYUSER_THREAD_LOCAL.set(loginUser); } }
JwtUtil(生成与解析token)
- 依赖jjwt-0.9.1.jar
public class JwtUtil { public static final String UID = "uid"; private static final String SECRET = "6A50A18D70FA63636645C65459F1D78A"; private static final long EXPIRE = 5 * 60 * 1000;//有效时间,5分钟 /** * 生成token * * @param claims:字符串中要保存的用户信息 * @return */ public static String generate(Map<String, Object> claims) { Date nowDate = new Date(); //过期时间 Date expireDate = new Date(nowDate.getTime() + EXPIRE); return Jwts.builder() .setClaims(claims) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); } /** * 解析token * * @param token * @return */ public static Claims getClaim(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); return claims; } }
-
最全SpringBoot下SpringSecurity账号和手机验证码登录多重认证实践
2020-12-11 23:05:35文章目录一: Spring Boot 引入Security 的 pom依赖1.1: 首先...JwtLoginFilter三: 手机号权限登录流程手机号登录认证流程过滤器 SmsCodeLoginFilter四: token 过滤认证校验token 认证过滤器 JwtTokenAuthenticationFil文章目录
前言: 坚持不是件容易的事. 距离上次发言已是半载. 最近更新了个人项目的登录权限模块;增加了手机号认证,到此实现了spring security 双重认证方式.目前可以自行注册体验.
一: Spring Boot 引入Security 的 pom依赖
1.1: 首先引入pom jar 包
spring boot使用security 只需引入以下配置,会自动拉取当前boot版本下的security依赖模块:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
当前项目SpringBoot 版本 2.0.4.RELEASE; 引入后可以看到 当前引入了 以下模块版本;spring-security-config;
spring-security-core;spring-security-web;<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.0.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>5.0.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.0.7.RELEASE</version> </dependency>
1.2: 配置 WebSecurityConfig
@EnableWebSecurity 作用 :
-
加载了WebSecurityConfiguration配置类, 配置安全认证策略。
在这个配置类中,注入了一个非常重要的bean, bean的name为: springSecurityFilterChain,这是Spring Secuity的核心过滤器, 这是请求的认证入口。 -
加载了AuthenticationConfiguration, 配置了认证信息。
这个类是来配置认证相关的核心类, 这个类的主要作用是,
向spring容器中注入AuthenticationManagerBuilder, AuthenticationManagerBuilder其实是使用了建造者模式,
他能建造AuthenticationManager, 这个类前面提过,是身份认证的入口。
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint; @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private MyLogoutSuccessHandler myLogoutSuccessHandler; @Resource private ApplicationEventPublisher applicationEventPublisher; @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { // 自定义 账号登录身份认证组件 auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService)); // 自定义 短信登录身份认证组件 auth.authenticationProvider(new SmsCodeAuthenticationProvider()); } //各类错误异常处理 以下针对于访问资源路径 认证异常捕获 和 无权限处理 /** * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { // 项目中用到iframe嵌入网页,然后用到springsecurity就被拦截了 浏览器报错 x-frame-options deny // 原因是因为springSecurty使用X-Frame-Options防止网页被Frame http.headers().frameOptions().disable(); // 禁用 csrf(Cross-site request forgery)跨站请求伪造, 由于使用的是JWT,我们这里不需要csrf //https://blog.csdn.net/yjclsx/article/details/80349906 //处理来自浏览器的请求需要是CSRF保护,如果后台服务是提供API调用那么可能就要禁用CSRF保护 http.cors().and().csrf().disable() .authorizeRequests() // 跨域预检请求 .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() ..... 等配置 http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint).accessDeniedHandler(myAccessDeniedHandler); // token 校验 http.addFilterBefore(new JwtTokenAuthenticationFilter(),AbstractPreAuthenticatedProcessingFilter.class); // 开启短信登录认证过滤器 http.addFilterBefore(new SmsCodeLoginFilter(authenticationManager(),myAuthenticationSuccessHandler,myAuthenticationFailureHandler,applicationEventPublisher),UsernamePasswordAuthenticationFilter.class); // 开启账号登录认证流程过滤器 http.addFilterBefore(new JwtLoginFilter(authenticationManager(),myAuthenticationSuccessHandler,myAuthenticationFailureHandler,applicationEventPublisher), UsernamePasswordAuthenticationFilter.class); // 退出登录处理器 清除redis 中token GET请求 http.logout().logoutUrl("/logout").logoutSuccessHandler(myLogoutSuccessHandler); // token 不保存session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } @Bean @Override public AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); }
在这个配置类中,我们主要做了以下几个配置:
-
1.访问路径URL的授权策略,如登录、静态资源、回调接口、Swagger访问免登录认证等
-
2.指定了账号登录认证流程过滤器 JwtLoginFilter,由它来触发账号登录认证
- 指定了账号登录自定义身份认证组件 JwtAuthenticationProvider,并注入 UserDetailsService
-
3.指定了短信登录认证流程过滤器 SmsCodeLoginFilter,由它来触发短信登录认证
- 指定了短信登录自定义身份认证组件 SmsCodeAuthenticationProvider
-
4.指定了token访问控制过滤器 JwtTokenAuthenticationFilter,在授权时解析令牌和设置登录状态
-
5.自定义认证成功处理器MyAuthenticationSuccessHandler;
- 认证失败处理器MyAuthenticationFailureHandler;
- 退出成功处理器MyLogoutSuccessHandler
-
6.自定义认证身份验证处理器MyAuthenticationEntryPoint;
- 访问拒绝处理器 MyAccessDeniedHandler
-
7.注入事件监听器 监听登录 退出等事件
二: 账号权限登录流程
账号登录认证流程过滤器 JwtLoginFilter
覆写认证方法,修改用户名、密码的获取方式 覆写认证成功后的操作,移除后台跳转,添加生成令牌并返回给客户端
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { public JwtLoginFilter(AuthenticationManager authManager, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler, ApplicationEventPublisher eventPublisher) { setAuthenticationManager(authManager); setAuthenticationSuccessHandler(successHandler); setAuthenticationFailureHandler(failureHandler); setApplicationEventPublisher(eventPublisher); } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // POST 请求 /login 登录时拦截, 由此方法触发执行登录认证流程,可以在此覆写整个登录认证逻辑 super.doFilter(req, res, chain); } /** * 此过滤器的用户名密码默认从request.getParameter()获取,但是这种 * 读取方式不能读取到如 application/json 等 post 请求数据,需要把 * 用户名密码的读取逻辑修改为到流中读取request.getInputStream() * 在此做验证码的验证 * @param request * @param response * @return * @throws AuthenticationException */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //获取请求实体 String body = HttpRequestUtil.getBody(request); JSONObject jsonObject = JSON.parseObject(body); String uuid = jsonObject.getString("uuid"); String code = jsonObject.getString("imgCode"); RedissonObject redissonObject = (RedissonObject)SpringContextUtils.getBeanByClass(RedissonObject.class); // 查询验证码 String redisCode = redissonObject.getValue(uuid); // 清除验证码 redissonObject.delete(uuid); if (StringUtils.isBlank(redisCode)) { logger.error("验证码不存在或已过期"); throw new MyAuthenticationException("验证码不存在或已过期"); } if (StringUtils.isBlank(code) || !code.equalsIgnoreCase(redisCode)) { logger.error("验证码错误"); throw new MyAuthenticationException("验证码错误"); } //账户和密码 String username = jsonObject.getString("username"); String password = jsonObject.getString("password"); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); JwtAuthenticatioToken authRequest = new JwtAuthenticatioToken(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
-
1: attemptAuthentication方法 把用户名密码的读取逻辑修改为到流中读取request.getInputStream() 校验验证码;
-
2:this.getAuthenticationManager().authenticate(authRequest) 账号和密码 传递到认证管理ProviderManager
-
3:ProviderManager 下循环遍历多个认证器provider;if (!provider.supports(toTest)) 判断当前 Authentication 是否支持
当前是账号登录;此处 JwtAuthenticatioToken.class.isAssignableFrom(aClass) 为true 进行用户名登录认证; -
4: 找到自定义的 JwtAuthenticationProvider 进行认证;此处可以复写部分功能扩展认证;再调用 父类AbstractUserDetailsAuthenticationProvider的super.authenticate(authentication)方法;
-
5:AbstractUserDetailsAuthenticationProvider 调用子类DaoAuthenticationProvider的retrieveUser()方法;
-
6:DaoAuthenticationProvider执行实际获取用户 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username)获取用户;
-
7:找到我们自定义实现 UserDetailsService的类 UserDetailsServiceImpl;通过用户名查询用户判断是否存在;查询权限封装;
-
8:AbstractUserDetailsAuthenticationProvider 再调用DefaultPreAuthenticationChecks检查当前用户preAuthenticationChecks.check(user)判断当前用户isAccountNonLocked()、isEnabled()、isAccountNonExpired()、isCredentialsNonExpired();
-
9:AbstractUserDetailsAuthenticationProvider再调用 DaoAuthenticationProvider的additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication)验证密码; -
10: 如果登录过程出现失败:MyAuthenticationFailureHandler将处理失败信息json回写;
- 登录成功触发:MyAuthenticationSuccessHandler onAuthenticationSuccess() 将登录信息记录到redis
-
11: 交互认证成功 监听器AuthenticationEventLogger 监听到事件 InteractiveAuthenticationSuccessEvent 记录登录日志;
三: 手机号权限登录流程
手机号登录认证流程过滤器 SmsCodeLoginFilter
手机号登录采用自己的认证器,对于认证结果成功和失败处理采用公共的处理器;
public class SmsCodeLoginFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_MOBILE_KEY = "mobile"; public static final String SPRING_SECURITY_CODE_KEY = "code"; private String mobileParameter = SPRING_SECURITY_MOBILE_KEY; private String codeParameter = SPRING_SECURITY_CODE_KEY; private boolean postOnly = true; public SmsCodeLoginFilter(AuthenticationManager authManager, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler, ApplicationEventPublisher eventPublisher) { super(new AntPathRequestMatcher("/mobile/login", "POST")); setAuthenticationManager(authManager); setAuthenticationSuccessHandler(successHandler); setAuthenticationFailureHandler(failureHandler); setApplicationEventPublisher(eventPublisher); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String body = HttpRequestUtil.getBody(request); JSONObject jsonObject = JSON.parseObject(body); String mobile = jsonObject.getString(mobileParameter); String code = jsonObject.getString(codeParameter); //校验code RedissonObject redissonObject = (RedissonObject)SpringContextUtils.getBeanByClass(RedissonObject.class); String msgCode = redissonObject.getValue(mobile); // 清除短信验证码 redissonObject.delete(mobile); if (StringUtils.isBlank(msgCode)) { logger.error("验证码不存在或已过期"); throw new MyAuthenticationException("验证码不存在或已过期"); } if (StringUtils.isBlank(code) || !code.equalsIgnoreCase(msgCode)) { logger.error("验证码错误"); throw new MyAuthenticationException("验证码错误"); } if (mobile == null) { mobile = ""; } if (code == null) { code = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile,code); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); }
-
1: attemptAuthentication方法,同样把手机号和验证码获取到,并此处校验验证码;
-
2: this.getAuthenticationManager().authenticate(authRequest)手机号和验证码 传递到认证管理ProviderManager
-
3 :ProviderManager 下循环遍历多个认证器provider;if (!provider.supports(toTest)) 判断当前 Authentication 是否支持
当前是账号登录;此处 SmsCodeAuthenticationToken.class.isAssignableFrom(aClass); 为true 进行短信登录认证; -
4: 找到自定义的 SmsCodeAuthenticationProvider 进行认证;此处相比用户名登录简化了没有用户密码判断;用户状态判断;直接查询用户权限
-
5: 如果登录过程出现失败:MyAuthenticationFailureHandler将处理失败信息json回写;
- 登录成功触发:MyAuthenticationSuccessHandler onAuthenticationSuccess() 将登录信息记录到redis
-
6: 交互认证成功 监听器AuthenticationEventLogger 监听到事件 InteractiveAuthenticationSuccessEvent 记录登录日志;
四: token 过滤认证校验
token 认证过滤器 JwtTokenAuthenticationFilter
由于前后分离的权限 无状态化 无session 我们将登录用户信息放入缓存 监控登录用户时效管理
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //获取登录接口 String requestUrl = request.getRequestURI(); // 如果不为登录接口 则处理token if (!requestUrl.endsWith("/login")) { // 获取token, 并检查登录状态 String token = request.getHeader("token"); // token未过期 if (token != null && !JwtTokenUtils.isTokenExpired(token)) { RedissonObject redissonObject = (RedissonObject) SpringContextUtils.getBeanByClass(RedissonObject.class); //缓存中拿取数据 JwtUserRedis jwtUserRedis = redissonObject.getValue("user:"+token); //没有获取到值 if (jwtUserRedis == null) { chain.doFilter(request,response); return; } UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtUserRedis.getUsername(),null,jwtUserRedis.getAuthorities()); // 认证放入线程 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request,response); }else{ chain.doFilter(request,response); } } }
-
对于非登录请求判断 拿取请求头中的 token判断是否过期;过期直接跳过后续会进行权限返回无法访问;
-
未过期则从redis 获取信息;如果为空直接跳过去执行后面过滤器;不为空则封装用户权限到当前线程认证;
-
当前线程存在登录用户信息后,后续可以进行校验通过操作;
完整项目代码以及体验地址:
项目代码: https://gitee.com/sinaC/youliao
体验地址: http://132.232.43.102 -
-
事实上在此我可以负责的告诉你,在sa-token中,登录授权就是如此的简单,不需要什么全局过滤器,不需要各种乱七八糟的配置!只需要这一行简单的API调用,即可完成会话的登录授权! 当你受够Shiro、Security等框架的...
-
【Shiro】用户名密码或手机号短信登录(多realm认证)
2018-09-13 11:56:14用户名密码使用的令牌自然是UsernamePasswordToken,我们可以继承UsernamePasswordToken,再添加上手机号属性,在不同的控制器中传入Token,然后由Realm判断当前的Token属于UsernamePasswordToken还是UsernamePa...
在登录认证中,经常需要实现用户名密码和手机号验证码这两种登录方式。最近学了Shiro,所以在这里记录下。
用户名密码使用的令牌自然是UsernamePasswordToken,我们可以参考UsernamePasswordToken,自定义PhoneToken,在不同的控制器中传入Token,然后由Realm判断当前的Token属于UsernamePasswordToken还是PhoneToken。
自定义Token:
public class PhoneToken implements HostAuthenticationToken, RememberMeAuthenticationToken, Serializable { // 手机号码 private String phone; private boolean rememberMe; private String host; /** * 重写getPrincipal方法 */ public Object getPrincipal() { return phone; } /** * 重写getCredentials方法 */ public Object getCredentials() { return phone; } public PhoneToken() { this.rememberMe = false; } public PhoneToken(String phone) { this(phone, false, null); } public PhoneToken(String phone, boolean rememberMe) { this(phone, rememberMe, null); } public PhoneToken(String phone, boolean rememberMe, String host) { this.phone = phone; this.rememberMe = rememberMe; this.host = host; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public String getHost() { return host; } @Override public boolean isRememberMe() { return rememberMe; } }
自定义Realm:
public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = null; if(authenticationToken instanceof UsernamePasswordToken){ token = (UsernamePasswordToken) authenticationToken; }else{ return null; } String username = token.getUsername(); if(StringUtils.isBlank(username)){ return null; } UserDO user = userService.getUser(token.getUsername()); // 账号不存在 if (user == null) { throw new CustomAuthenticationException("账号或密码不正确"); } // 账号锁定 if (user.getStatus() == 1) { throw new CustomAuthenticationException("账号已被锁定,请联系管理员"); } // 主体,一般存用户名或用户实例对象,用于在其他地方获取当前认证用户信息 Object principal = user; // 凭证,这里是从数据库取出的加密后的密码,Shiro会用于与token中的密码比对 Object hashedCredentials = user.getPassword(); // 以用户名作为盐值 ByteSource credentialsSalt = ByteSource.Util.bytes(token.getUsername()); return new SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, this.getName()); } // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } @Override public boolean supports(AuthenticationToken var1){ return var1 instanceof UsernamePasswordToken; } }
public class PhoneRealm extends AuthorizingRealm { @Resource UserService userService; // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { PhoneToken token = null; // 如果是PhoneToken,则强转,获取phone;否则不处理。 if(authenticationToken instanceof PhoneToken){ token = (PhoneToken) authenticationToken; }else{ return null; } String phone = (String) token.getPrincipal(); UserDO user = userService.selectByPhone(phone); if (user == null) { throw new CustomAuthenticationException("手机号错误"); } return new SimpleAuthenticationInfo(user, phone, this.getName()); } // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return null; } @Override public boolean supports(AuthenticationToken var1){ return var1 instanceof PhoneToken; } }
控制器中的使用:
@RequestMapping("/user") @Controller public class UserController { // 用户名密码登录 @PostMapping("/dologin") @ResponseBody public BackAdminResult dologin(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) throws AuthenticationException { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); subject.login(token); UserDO user = (UserDO) subject.getPrincipal(); session.setAttribute(Constants.LOGIN_ADMIN_KEY, user); subject.getSession().setAttribute(Constants.LOGIN_ADMIN_KEY, user); return BackAdminResult.build(0, "登录成功!"); } // 使用手机号和短信验证码登录 @RequestMapping("/plogin") @ResponseBody public BackAdminResult pLogin(@RequestParam("phone") String phone, @RequestParam("code") String code, HttpSession session){ // 根据phone从session中取出发送的短信验证码,并与用户输入的验证码比较 String messageCode = (String) session.getAttribute(phone); if(StringUtils.isNoneBlank(messageCode) && messageCode.equals(code)){ UserNamePasswordPhoneToken token = new UserNamePasswordPhoneToken(phone); Subject subject = SecurityUtils.getSubject(); subject.login(token); UserDO user = (UserDO) subject.getPrincipal(); session.setAttribute(Constants.LOGIN_ADMIN_KEY, user); return BackAdminResult.build(0, "登录成功!"); }else{ return BackAdminResult.build(2, "验证码错误!"); } } }
配置(部分):
@Configuration public class ShiroConfig { /** * 加密策略 */ @Bean public CredentialsMatcher credentialsMatcher(){ HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 加密算法:MD5、SHA1 credentialsMatcher.setHashAlgorithmName(Constants.Hash_Algorithm_Name); // 散列次数 credentialsMatcher.setHashIterations(Constants.Hash_Iterations); return credentialsMatcher; } /** * 自定义Realm */ @Bean public UserRealm userRealm(CredentialsMatcher credentialsMatcher) { UserRealm userRealm = new UserRealm(); userRealm.setCredentialsMatcher(credentialsMatcher); userRealm.setCacheManager(shiroCacheManager()); return userRealm; } @Bean public PhoneRealm phoneRealm(){ PhoneRealm phoneRealm = new PhoneRealm(); phoneRealm.setCacheManager(shiroCacheManager()); return phoneRealm; } /** * 认证器 */ @Bean public AbstractAuthenticator abstractAuthenticator(UserRealm userRealm, PhoneRealm phoneRealm){ // 自定义模块化认证器,用于解决多realm抛出异常问题 ModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator(); // 认证策略:AtLeastOneSuccessfulStrategy(默认),AllSuccessfulStrategy,FirstSuccessfulStrategy authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy()); // 加入realms List<Realm> realms = new ArrayList<>(); realms.add(userRealm); realms.add(phoneRealm); authenticator.setRealms(realms); return authenticator; } @Bean public SecurityManager securityManager(UserRealm userRealm, PhoneRealm phoneRealm, AbstractAuthenticator abstractAuthenticator) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置realms List<Realm> realms = new ArrayList<>(); realms.add(userRealm); realms.add(phoneRealm); securityManager.setRealms(realms); // 自定义缓存实现,可以使用redis securityManager.setCacheManager(shiroCacheManager()); // 自定义session管理,可以使用redis securityManager.setSessionManager(sessionManager()); // 注入记住我管理器 securityManager.setRememberMeManager(rememberMeManager()); // 认证器 securityManager.setAuthenticator(abstractAuthenticator); return securityManager; } }
自定义异常:
public class CustomAuthenticationException extends AuthenticationException { // 异常信息 private String msg; public CustomAuthenticationException(String msg){ super(msg); this.msg = msg; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } }
自定义异常处理:
/** * 用于捕获和处理Controller抛出的异常 */ @ControllerAdvice public class GlobalExceptionHandler { private final static Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(CustomAuthenticationException.class) @ResponseBody public BackAdminResult handleAuthentication(Exception ex){ LOG.info("Authentication Exception handler " + ex.getMessage() ); return BackAdminResult.build(1, ex.getMessage()); } }
这里有个问题,就是默认的ModularRealmAuthenticator在处理多realm时,会把异常捕获,导致自定义异常处理器捕获不到认证时抛出的异常,所以需要重写ModularRealmAuthenticator的doMultiRealmAuthentication方法,把异常抛出来。
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator { /** * 重写doMultiRealmAuthentication,抛出异常,便于自定义ExceptionHandler捕获 */ @Override public AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) throws AuthenticationException { AuthenticationStrategy strategy = this.getAuthenticationStrategy(); AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); Iterator var5 = realms.iterator(); while(var5.hasNext()) { Realm realm = (Realm)var5.next(); aggregate = strategy.beforeAttempt(realm, token, aggregate); if (realm.supports(token)) { AuthenticationInfo info = null; Throwable t = null; info = realm.getAuthenticationInfo(token); aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } } aggregate = strategy.afterAllAttempts(token, aggregate); return aggregate; } }
在配置那里把自定义的ModularRealmAuthenticator替换默认的即可。
-
Java微信开发测试号校验TOKEN配置获取微信用户基本信息
2020-07-08 12:22:05访问微信测试号登录页面,通过打开自己手机的微信,扫一扫登录 https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login ***在Java程序里面编写servlet以响应微信号 在测试号里面设置接口配置信息的URL,... -
google验证手机号码无法验证_Google验证器是如何实现的?
2020-11-28 00:26:34常见的使用场景:Google Apps验证公司的软token登录服务器应用:Authy2. Google验证器实现原理Google身份验证系统是通过基于时间的一次性密码算法实现的双因子(2FA)验证,即TOTP(Time-Based O... -
关于jwt token失效的坑.
2018-05-23 16:07:42最近项目中使用jwt token作为客户端发送请求的验证,自己一直没有深入研究源码,造成了修改用户的手机号后导致token失效.前端无法进行二次请求,每次请求都是401未授权,一开始以为是token过期,后来查看源码发现是生成... -
Remote URL test failed: remote: Incorrect username or password (access token0m Aut
2020-11-20 17:02:15码云的用户名和手机号或者邮箱都是可以的,不过你使用手机号或者邮箱的前提是你已经在码云上面绑定了手机号和邮箱,密码就是你平时登录所用的密码 解决办法: 打开控制面板,搜索凭据管理器,然后点击凭据管理器: ... -
spring boot整合spring security(二)--手机号验证码登陆
2020-11-11 17:41:39手机号验证码登陆概述搭建基础架构认证流程定义token定义过滤器filter接下来是自定义手机号验证码登陆鉴证器provider修改UserDetailService的实现类为生成验证码接口登陆接口测试未登录状态用户名 密码登陆手机号 ... -
sso登录
2017-09-28 09:55:32这里是利用了sso的接口文档,即校验接口、注册、登录接口、根据token查询用户接口、安全退出接口。 这个的调用服务层是利用jsonp的形式访问的服务接口,实现跨域访问。客户端全部在jsp页面实现的。 具体... -
SpringBoot OAuth2.0 使用短信验证码登录授权
2021-03-26 18:04:05SpringBoot OAuth2.0 使用...校验手机号 + 短信验证码是否一致 getOAuth2Authentication, 手机和验证码校验无误, 返回 OAuth2Authentication 授权信息(access_token) 创建 SmsCodeTokenGranter 继承 AbstractTo -
Jmeter 中多线程并发和循环控制器
2019-01-02 14:42:00给一个手机号发送短信验证码,通过正确输入短信验证码即登录并获得token,进行其他操作。 短信验证码是4位,即9999个组合, 接口没有对验证次数做校验,所以可以一直一直尝试通过验证码登录。 起初在想怎么用... -
OkHttp中Interceptor拦截器之公共参数请求封装
2018-01-03 02:45:59之前在面试的时候遇到这样的一个问题,那就是如果app中所有的请求都要加入一些参数(例如 版本号、手机号、登录用户名、token等。。。)那么怎么做才能实现,而不至于在每次请求的时候都去进行添加这些请求头。其实... -
Twilio电话拨号器和CRM助手「Twilio Phone Dialer and CRM Helper」-crx插件
2021-03-08 17:53:40用您的Twilio SID和AUTH TOKEN登录,并提供一个唯一的客户名称。 设置你的默认号码。 可选择设置您的CRM细节。 您的应用程序已准备就绪 主要特征: - 为许多国家购买Twilio号码。 - 打电话给世界上的任何号码。 -... -
Java+Selenium+swing_淘宝首页数据爬取的小软件
2020-06-02 18:02:20发现都是换取到token令牌后,然后去访问淘宝进行爬取的,感觉太麻烦了,换了一个比较傻瓜式的方法。 使用java+selenium+swing做的一个小桌面软件,用于爬取淘宝首页数据。 淘宝搜索商品爬取项目说明界面说明流程... -
基于PHP实现短信验证码接口(容联运通讯)
2021-01-20 01:10:111、登录荣联运通讯注册获取ACCOUNT SID、AUTH TOKEN、Rest URL(生产)、AppID(默认); 2、注册测试用手机号码(先注册测试号码方可使用); 3、下载demo示例,并将代码放到项目中(最好单独建文件夹存储)。 代码... -
短信验证码接口(容联运通讯)
2016-09-06 10:18:00步骤:1、登录荣联运通讯注册获取ACCOUNT SID、AUTH TOKEN、Rest URL(生产)、AppID(默认); 2、注册测试用手机号码(先注册测试号码方可使用); 3、下载demo示例,并将代码放到项目中(最好单独建文件夹存储... -
Java商城面试题(三)
2018-09-16 16:09:59SSO单点登录: SSO系统:这里涉及到拦截器。 这里是利用了sso的接口文档,即校验接口、注册、登录接口、根据token查询用户接口、安全退出接口。 这个的调用服务层是...检验用户名是否存在、手机号和邮箱不能... -
显示个人信息,例如用户名、真实姓名、宿舍号、学号等,显示之后还需要支持对于数据进行修改,修改之后,要同步修改页面的信息,这需要用到Ajax进行数据的提交,并且进行页面的局部刷新。 2.1.8 我发布的商品模块...
-
Google Android SDK开发范例大全(第3版) 1/5
2013-01-20 18:45:35完备的Google网络服务:Google语音搜寻、Google远程账号登录、Google Search API、Google Chart API、Google Picasa手机相册、Google Translate API整合等。 Google Map应用:GPS定位、规划导航路径、GPS Google地图... -
Google Android SDK开发范例大全(第3版) 4/5
2013-01-20 19:16:09完备的Google网络服务:Google语音搜寻、Google远程账号登录、Google Search API、Google Chart API、Google Picasa手机相册、Google Translate API整合等。 Google Map应用:GPS定位、规划导航路径、GPS Google地图... -
Google Android SDK开发范例大全(第3版) 5/5
2013-01-20 19:30:04完备的Google网络服务:Google语音搜寻、Google远程账号登录、Google Search API、Google Chart API、Google Picasa手机相册、Google Translate API整合等。 Google Map应用:GPS定位、规划导航路径、GPS Google地图... -
Google Android SDK开发范例大全(第3版) 3/5
2013-01-20 19:09:12完备的Google网络服务:Google语音搜寻、Google远程账号登录、Google Search API、Google Chart API、Google Picasa手机相册、Google Translate API整合等。 Google Map应用:GPS定位、规划导航路径、GPS Google地图... -
Google Android SDK开发范例大全(第3版) 2/5 添加目录
2013-01-20 18:52:49完备的Google网络服务:Google语音搜寻、Google远程账号登录、Google Search API、Google Chart API、Google Picasa手机相册、Google Translate API整合等。 Google Map应用:GPS定位、规划导航路径、GPS Google地图... -
Discuz新秀网络验证系统-UTF8-ver5.2 - 2020.2.13
2020-02-13 19:23:57框架简介: ... 本框架采用成熟稳定的Discuz3.4论坛系统,MVC框架环境下开发; 本框架无缝整合Discuz用户系统内置功能,采用插件形式开发...手机模块:短信发送、手机绑定账号、手机登录 QQ模块:QQ登录 后续功能…… -
Discuz新秀网络验证系统-GBK-ver5.2 - 2020.2.13
2020-02-13 19:22:43框架简介: ... 本框架采用成熟稳定的Discuz3.4论坛系统,MVC框架环境下开发; 本框架无缝整合Discuz用户系统内置功能,采用插件形式开发...手机模块:短信发送、手机绑定账号、手机登录 QQ模块:QQ登录 后续功能……