網(wǎng)站公安備案增加開辦主體十大營(yíng)銷模式
兩種常見的認(rèn)證方案
基于Session認(rèn)證
- 登錄狀態(tài)信息保存在服務(wù)器內(nèi)存中,若訪問量增加,單臺(tái)節(jié)點(diǎn)壓力會(huì)較大
- 集群環(huán)境下需要解決集群中的各種服務(wù)器登錄狀態(tài)共享問題
解決方案:將登錄狀態(tài)保存的Redis中,從Redis中查找登錄狀態(tài)
基于Token認(rèn)證
- 登錄狀態(tài)保存在客戶端,服務(wù)器沒有存儲(chǔ)開銷
- 客戶端發(fā)起的每個(gè)請(qǐng)求自身均攜帶登錄狀態(tài),所以即使后臺(tái)為集群,也不會(huì)面臨登錄狀態(tài)共享的問題
Token登錄方案
登錄流程如下
登錄管理使用的三個(gè)接口
- 獲取圖形驗(yàn)證碼,通過easy-captcha依賴引入工具
- 登錄,通過jjwt-api jjwt-impl jjwt-jackson 依賴引入工具
- 獲取登錄用戶個(gè)人信息
為了使得所有受保護(hù)的接口增加驗(yàn)證JWT合法性邏輯,通過HandlerInterceptor攔截實(shí)現(xiàn)
common模塊中,創(chuàng)建JWT工具類
public class JwtUtil{private static SecretKey secretKey = Keys.hmacShaKeyFor("任意復(fù)雜字符串大于指定字節(jié)數(shù)".getBytes());public static String createToken(Long userId,String username){//官方設(shè)置用set,自定義設(shè)置用claimString jwt = Jwts.builder().setExpiration(new Date(System.currentTimeMillis()+3600000)).setSubject("LOGIN_USER").claim("userId",userId).claim("username",username).signWith(secretKey,SignatureAlgorithm.HS256)//設(shè)置簽名以及所使用的算法種類.compact();return jwt;}//校驗(yàn)token是否合法public static Claims parseToken(String token){if(token==null){throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);}try{JwtParser jwtParser = JWTS.parserBuilder().setSigningKey(secretKey).build();Jws<Claims> claimsJws = jwtParser.parse(token);return claimsJws.getBody();}catch(ExpiredJwtExpection e){throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);}catch(JwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);}}
}
①、Controller接口
@Tag(name="后臺(tái)管理系統(tǒng)登錄")
@RestController
@RequestMapping("/admin")
public class LogicController{@Autowiredprivate LoginService service;@Operation(summary="獲取圖形驗(yàn)證碼")@GetMapping("login/captcha")public Result<CaptchaVo> getCaptcha(){CaptchaVo result = servcie.getCaptcha();return Result.ok(result);}@Operation(summary="登錄")@GetMapping("login")public Result<String> login(@RequestBody Login loginVo){String jwt = service.login(loginVo);return Result.ok(jwt);}@Operation(summary="獲取登錄用戶個(gè)人信息")@GetMapping("info")public Result<SystemUserInfoVo> info(@RequestHeader("access-token") String token){//將請(qǐng)求的某個(gè)header綁定到tokenClaims claims = JwtUtil.parseToken(token);Long userId = claims.get("userId",Long.class);SystemUserInfoVo systemUserInfoVo = service.getLoginUserInfoById(userId);return Result.ok(systemUserInfoVo);}
}
②、接口及其實(shí)現(xiàn)類
@Service
public class LoginServiceImpl implements LoginService{@Autowiredpirvate String RedisTemplate stringRedisTemplate;@Autowiredprivate SystemUserMapper systemUserMapper;@Overridepublic CaptchaVo getCaptcha(){SpecCaptcha specCaptcha = new SpecCaptcha(130,48,4);String code = specCaptcha.text().toLowerCase();//Redis中的key命名——項(xiàng)目名:功能模塊名 admin:loginString key = "admin:login:" + UUID.randomUUID();//后臺(tái)管理系統(tǒng)登錄模塊stringRedisTemplate.opsForValue().set(key,code,60,TimeUnit.SECONDS)return new CaptchaVo(specCpatcha.toBase64(),key);}@Overridepublic String login(LoginVo loginVo){if(loginVo.getCaptchaCode()==null){throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOTFOUND);}//從Redis獲取保存的驗(yàn)證碼String code = stringRedisTemplate.opsForValue().get(loginVo.getCaptchaKey());if(code==null){//驗(yàn)證碼過期throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);}if(!code.equals(loginVo.getCaptchaCode().toLowerCase())){throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);}//LambdaQueryWrapper<SystemUser> queryWrapper = new LambdaQueryWrapper<>();//queryWrapper.eq(SystemUser::getUsername,loginVo.getUsername());/**由于該接口使用通用查詢會(huì)導(dǎo)致和實(shí)體類的屬性注解select=false查詢功能,不顯示密碼字段的沖突該接口出現(xiàn)了空指針異常所以采用自定義查詢*///SystemUser systemUser = systemUserMapper.selectOne(queryWrapper);SystemUser systemUser = systemUserMapper.selectOneByUsername(queryWrapper);if(systemUser==null){throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);}if(systemUser.getStatus()==BaseStatus.DISABLE){throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);}if(systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword))){throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);}//生成JWTreturn JwtUtil.createToken(systemUser.getId(),systemUser.getUsername());}@Overridepublic SystemUserInfoVo getLoginUserInfoById(Long userId){SystemUser systemUser = systemUserMapper.selectById(userId);SystemUserInfo systemUserInfoVo = new SystemUserInfo();systemUserInfoVo.setName(systemUser.getName());systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl());return systemUserInfoVo;}
}
③、為登錄之后才能訪問的接口增加驗(yàn)證JWT合法性的邏輯(攔截器)
@Component
public class AuthenticationInterceptor implements HandlerInterceptor{//Controller接口之前執(zhí)行@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResposne response,Object handler)throw Exception{//從請(qǐng)求頭獲取tokenString token = request.getHeader("access-token");//同前端約定好的鍵值對(duì)JwtUtil.parseToken(token);return true;}
}
注冊(cè)攔截器
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer{@Autowiredprivate AuthenticationIntercpetor authenticationIntercpetor;@Overridepublic void addInterceptor(InterceptorRegistry registry){registry.addInterceptor(this.authenticationIntercpetor).addPathPatterns("/admin/**")//攔截admin下所有路徑.excludePathPatterns("/admin/login/**");}
}
關(guān)于攔截器的重復(fù)解析(一次在攔截器中,一次是controller接口中)
JwtUtil.parseToken(token)
所以要通過攔截器的解析結(jié)果保存起來,由于攔截器限制性,通常將結(jié)果保存在ThreadLocal中
①、在common模塊中創(chuàng)建工具類
public class LoginUserHolder{public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();public static void setLoginUser(LoginUser loginUser){threadLocal.set(loginUser);}public static LoginUser getLoginUser(){return threadLocal.get();}public static void clear(){threadLocal.remove();}
}
同時(shí)創(chuàng)建LoginUser
@Data
@AllArgsConstrctor
public class LoginUser{private Long userId;private String username;
}
②、修改攔截器
@Component
public class AuthenticationInterceptor implements HandlerInterceptor{@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throw Exception{String token = request.getHeader("access-token");Claims claims = JwtUtil.parseToken(token);Long userId = claims.get("userId",Long.class);String username = claims.get("username",String.class);LoginUserHolder.setLoginUser(new LoginUser(userId,username));return true;}//清理線程池的內(nèi)容@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler){LoginUserHolder.clear();}
}
③、改造Controller
@Operation(sumary="獲取登錄用戶個(gè)人信息")
@GetMapping("info")
public Result<SystemUserInfoVo> info(){Long userId = LoginUserHolder.getLoginUser().getUserId();SystemUserInfoVo systemUserInfo = service.getLoginUserInfoById(userId);return Result.ok(systemUserInfoVo);
}
手機(jī)移動(dòng)端登錄的具體流程
①、Controller
@Tag(name="登錄管理")
@RestController
@RequestMapping("/app/")
public class LoginController{@Autowiredprivate LoginService service;@GetMapping("login/getCode")@Operation(summary="獲取短信驗(yàn)證碼")public Result getCode(@RequestParam String phone){service.getCode(phone);return Result.ok();}@PostMapping("login")@Operation(summary="登錄")public Result<String> login(@RequestBody LoginVo loginVo){return Result.ok()}@GetMapping("info")@Operation(summary="獲取登錄用戶信息")public Result<UserInfoVo> info(){retur Result.ok();}
}
②、service
Ⅰ、登錄的service及其實(shí)現(xiàn)類
public interface LoginService{void getCode(String phone);
}
@Service
public class LoginServiceImpl implements LoginService{@Autowiredprivate SmsService smsService;@Autowiredprivate StringRedisTemplate redisTemplate;@Overridepublic void getCode(String phone){}
}
Ⅱ、發(fā)送短信的service及其實(shí)現(xiàn)類
public interface SmsService{void sendCode(String phone,String code);
}
@Service
public class SmsServiceImpl implements SmsService{@Autowiredprivate Client client;@Overridepublic void sendCode(String phone,String code){SendSmsRequest request = new SendSmsRequest();requst.setPhoneNumber(phone);request.setSignName("阿里云短信測(cè)試");request.setTemplateCode("SMS_154950909");request.setTemplateParam("{\"code\":\""+code+"\"}");try{client.sendSms(request);}catch(Exception e){throw new RuntimeException(e);}}
}
調(diào)用阿里云短信服務(wù),common模塊中依賴:dysmsapi20170525
applicaiton.yml增加:access-key-id(賬號(hào)) access-key-secret(密碼) endpoint
對(duì)配置類進(jìn)行映射,common模塊中
@Data
@ConfigurationProperties(prefix="aliyun.sms")
public class AliyunSMSProperties{private String accessKeysId;private String accessKeySecret;private String endpoint;
}
將映射到的參數(shù)進(jìn)行創(chuàng)建訪問阿里云的客戶端配置
@Configuration
@EnableConfigurationProperties(AliyunSMSProperties.class)
@ConditionalOnProperty(name="aliyun.sms.endpoint")//條件注解,放置其他模塊沒有配置訪問參數(shù)而報(bào)錯(cuò)
public class AliyunSMSConfiguration{@Autowiredprivate AliyunSMSProperties properties;@Beanpublic Client createClient(){Config config = new Config();config.setAccessKeyId(properties.getAccessKeyId());config.setAccessKeySecret(properties.getAccessKeySecret());config.setEndpoint(properties.getEndpoint())try{return new Client(config);}catch(Exception e){throw new RuntimeException(e);}}
}