我們接著進行抽獎系統的完善。
前面我們完成了
1.結構初始化(統一結果返回之類的,還有包的分類)
2.加密(基于Hutool進行的對稱與非對稱加密)
3.用戶注冊
接下來我們先完善一下結構(統一異常處理)
1.統一異常處理
很簡單,@RestControllerAdvice+@ExceptionHandler即可
@RestControllerAdvice//可以捕獲全局拋出的異常
@ResponseBody
public class GlobalExceptionHandler {private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);//捕獲Service層的異常@ExceptionHandler(value = ServiceException.class)public CommonResult<?> serviceException(ServiceException e) {logger.error("serviceException :", e);//返回數據return CommonResult.error(GlobalErrorCodeConstant.INTERNAL_SERVICE_ERROR);}//捕獲Controller層的異常@ExceptionHandler(value = ControllerException.class)public CommonResult<?> controllerException(ControllerException e) {logger.error("controllerException :", e);//返回數據return CommonResult.error(GlobalErrorCodeConstant.INTERNAL_SERVICE_ERROR);}//捕獲全局的異常@ExceptionHandler(value = Exception.class)public CommonResult<?> exception(Exception e) {logger.error("exception :", e);//返回數據return CommonResult.error(GlobalErrorCodeConstant.INTERNAL_SERVICE_ERROR);}
}
這里使用@ExceptionHandler(value = 類名)的方式捕獲異常。
其他沒什么要特別注意的,補充GlobalErrorCodeConstant的異常種類
2.登錄模塊
這里提供短信驗證碼登錄的方式,因此我們要了解一下阿里短信服務
但是現在個人申請不到了,所以我們直接使用虛擬的驗證碼吧。
附一個申請成功后植入的代碼:
1.阿里短信代碼模塊:
依賴:
<dependency><groupId>com.aliyun</groupId><artifactId>dysmsapi20170525</artifactId><version>2.0.24</version>
</dependency>
短信服務工具類:
@Component
public class SMSUtil {private static final Logger logger = LoggerFactory.getLogger(SMSUtil.class);@Value(value = "${sms.sign-name}")private String signName;@Value(value = "${sms.access-key-id}")private String accessKeyId;@Value(value = "${sms.access-key-secret}")private String accessKeySecret;/*** 發送短信** @param templateCode 模板號* @param phoneNumbers 手機號* @param templateParam 模板參數 {"key":"value"}*/public void sendMessage(String templateCode, String phoneNumbers, String templateParam) {try {Client client = createClient();com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new SendSmsRequest().setSignName(signName).setTemplateCode(templateCode).setPhoneNumbers(phoneNumbers).setTemplateParam(templateParam);RuntimeOptions runtime = new RuntimeOptions();SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);if (null != response.getBody()&& null != response.getBody().getMessage()&& "OK".equals(response.getBody().getMessage())) {logger.info("向{}發送信息成功,templateCode={}", phoneNumbers, templateCode);return;}logger.error("向{}發送信息失敗,templateCode={},失敗原因:{}",phoneNumbers, templateCode, response.getBody().getMessage());} catch (TeaException error) {logger.error("向{}發送信息失敗,templateCode={}", phoneNumbers, templateCode, error);} catch (Exception _error) {TeaException error = new TeaException(_error.getMessage(), _error);logger.error("向{}發送信息失敗,templateCode={}", phoneNumbers, templateCode, error);}}/*** 使用AK&SK初始化賬號Client* @return Client*/private Client createClient() throws Exception {// 工程代碼泄露可能會導致 AccessKey 泄露,并威脅賬號下所有資源的安全性。以下代碼示例僅供參考。// 建議使用更安全的 STS 方式,更多鑒權訪問方式請參見:https://help.aliyun.com/document_detail/378657.html。Config config = new Config().setAccessKeyId(accessKeyId).setAccessKeySecret(accessKeySecret);// Endpoint 請參考 https://api.aliyun.com/product/Dysmsapiconfig.endpoint = "dysmsapi.aliyuncs.com";return new Client(config);}}
配置:
### 短信 ##
## 短信 ##
sms.access-key-id="填寫??申請的"
sms.access-key-secret="填寫??申請的"
sms.sign-name="填寫??申請的"
代碼使用:
使用時傳入3個參數:
templateCode(模板號):SMS_465324787
phoneNumbers(手機號):傳入你申請的
templateParam(模版參數):發送驗證碼的格式設置為{"key":"value"}
使用時傳入一個map并將其序列化
2.驗證碼模塊
利用Hutool工具:(這工具真好用都不用手寫了)
//生成隨機驗證碼
public class CaptchaUtil {/*** 生成隨機驗證碼** @param length 幾位* @return*/public static String getCaptcha(int length) {// 自定義純數字的驗證碼(隨機4位數字,可重復)RandomGenerator randomGenerator = new RandomGenerator("0123456789", length);LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);lineCaptcha.setGenerator(randomGenerator);// 重新生成codelineCaptcha.createCode();return lineCaptcha.getCode();}}
3.Controller
基于手機號生成驗證碼并發送驗證碼? ?最后使用Redis緩存驗證碼用于校驗
/*** 發送驗證碼* @param phoneNumber* @return*/@RequestMapping("/verification-code/send")public CommonResult<Boolean> verificationCode (String phoneNumber) {//日志打印logger.info("verificationCode phoneNumber:{}", phoneNumber);verificationCodeService.sendVerificationCode(phoneNumber);return CommonResult.success(Boolean.TRUE);}
4.Service
/*** 發送驗證碼* @param phoneNumber* @return*/public String sendVerificationCode(String phoneNumber);/*** 獲取驗證碼* @param phoneNumber* @return*/public String getVerificationCode(String phoneNumber);
@Overridepublic String sendVerificationCode(String phoneNumber) {//校驗手機號if(!RegexUtil.checkMobile(phoneNumber)) {throw new ServiceException(ServiceErrorCodeConstant.PHONE_NUMBER_ERROR);}//生成隨機驗證碼 利用hutool生成String code = CaptchaUtil.getCaptcha(4);//發送驗證碼Map<String, String> map = new HashMap<>();map.put("code", code);smsUtil.sendMessage(PHONE_NUMBER_TEMPLATE_CODE, phoneNumber, JacksonUtil.writeValueAsString(map));//緩存驗證碼redisUtil.set(PHONE_NUMBER_PRE +phoneNumber, code, PHONE_NUMBER_TIMEOUT);return redisUtil.get(PHONE_NUMBER_PRE +phoneNumber);}@Overridepublic String getVerificationCode(String phoneNumber) {//校驗手機號if(!RegexUtil.checkMobile(phoneNumber)) {throw new ServiceException(ServiceErrorCodeConstant.PHONE_NUMBER_ERROR);}return redisUtil.get(PHONE_NUMBER_PRE +phoneNumber);}
Redis使用簡單介紹:
配置:
在Linux服務器上通過隧道開放Redis的6379端口號
在Linux上輸入命令啟動Redis:
service redis-server start
?可以在idea上下載插件Redis Helper
刷新后在右側找到插件點擊加號添加Redis服務?
1.名稱(隨便,便于標識)
2.本機就行
3.在Linux隧道綁定的端口號
4.Test測試驗證,出現綠色即成功連接Redis?
?Redis使用測試:
@Testvoid redisTest() {stringRedisTemplate.opsForValue().set("key1", "value2");System.out.println("從redis中獲取value : " + stringRedisTemplate.opsForValue().get("key1"));}@Testvoid redisUtil() {
// redisUtil.set("key2", "value2");
// redisUtil.set("key3", "value3", 20L);
// System.out.println("key2是否存在: " + redisUtil.hasKey("key2"));
// System.out.println("key3是否存在: " + redisUtil.hasKey("key3"));// redisUtil.delete("key2");
// System.out.println("key2是否存在: " + redisUtil.hasKey("key2"));System.out.println("key3是否存在: " + redisUtil.hasKey("key3"));}
1.使用前注入StringRedisTemplate類
2.使用stringRedisTemplate.opsForValue().set("key1", "value2");添加元素
但是,每次使用都要注入StringRedisTemplate有點麻煩了,我們將其封裝成一個util工具。
@Configuration
public class RedisUtil {public static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);/*** StringRedisTemplate : 直接用String存儲讀取(可讀)* RedisTemplate : 先將被存儲數據轉換為字節數組(不可讀) 再存儲到Redis中 讀取時以字節數組讀取*/@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 設置值* @param key* @param value* @return*/public boolean set(String key,String value) {try{stringRedisTemplate.opsForValue().set(key, value);return true;}catch(Exception e) {logger.error("RedisUtil set 錯誤:({}, {})", key, value, e);return false;}}/*** 設置帶有過期時間的值* @param key* @param value* @param time 單位:秒* @return*/public boolean set(String key,String value, Long time) {try{stringRedisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);return true;}catch(Exception e) {logger.error("RedisUtil set 錯誤:({}, {}, {})", key, value, time, e);return false;}}/*** 獲取值* @param key* @return*/public String get(String key) {try{return StringUtils.hasText(key)? stringRedisTemplate.opsForValue().get(key): null;}catch(Exception e) {logger.error("RedisUtil set 錯誤:({})", key, e);return null;}}/*** 刪除值* @param key* @return*/public boolean delete(String... key) {try{if(key != null && key.length > 0) {if(key.length == 1) {stringRedisTemplate.delete(key[0]);}else {stringRedisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));}}return true;}catch(Exception e) {logger.error("RedisUtil delete 錯誤:({})", key, e);return false;}}/*** 查看Key是否存在* @param key* @return*/public boolean hasKey(String key) {return StringUtils.hasText(key)? stringRedisTemplate.hasKey(key): false;}}
該類提供了:
set:兩個,一個是傳入key:value? 。另一個是傳入key:value+過期時間
get:通過key獲取value
delete:通過key,進行批量刪除
hasKey:查看key是否存在
5.JWT令牌驗證
前面我們發送了驗證碼,在客戶端我們就要將該驗證碼輸入進行登錄,但還有一些用戶會采用手機號/郵箱+密碼的方式登錄。所以我們要開放兩個接口用于登錄。
登錄完后端會返回給前端一個token用于令牌校驗,使驗證碼登錄在多臺主機下都可以使用。
JWT本身沒什么說的,直接上代碼:
public class JWTUtil {private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);/*** 密鑰:Base64編碼的密鑰*/private static final String SECRET = "SDKltwTl3SiWX62dQiSHblEB6O03FG9/vEaivFu6c6g=";/*** 生成安全密鑰:將一個Base64編碼的密鑰解碼并創建一個HMAC SHA密鑰。*/private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET));/*** 過期時間(單位: 毫秒)*/private static final long EXPIRATION = 60*60*1000;/*** 生成密鑰** @param claim {"id": 12, "name":"張山"}* @return*/public static String genJwt(Map<String, Object> claim){//簽名算法String jwt = Jwts.builder().setClaims(claim) // 自定義內容(載荷).setIssuedAt(new Date()) // 設置簽發時間.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 設置過期時間.signWith(SECRET_KEY) // 簽名算法.compact();return jwt;}/*** 驗證密鑰*/public static Claims parseJWT(String jwt){if (!StringUtils.hasLength(jwt)){return null;}// 創建解析器, 設置簽名密鑰JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);Claims claims = null;try {//解析tokenclaims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();}catch (Exception e){// 簽名驗證失敗logger.error("解析令牌錯誤,jwt:{}", jwt, e);}return claims;}/*** 從token中獲取用戶ID*/public static Integer getUserIdFromToken(String jwtToken) {Claims claims = JWTUtil.parseJWT(jwtToken);if (claims != null) {Map<String, Object> userInfo = new HashMap<>(claims);return (Integer) userInfo.get("userId");}return null;}
}
提供了三個方法:
1.getJWT:通過傳入一個map生成token并返回。
2.parseJWT:將token解析為map數據
3.getUserIdFromToken:從token中獲取用戶ID(可能有用)
6.管理員登錄(兩種?式)
學習令牌的使?之后, 接下來我們通過令牌來完成??的登錄的流程
1. 登陸??把??名密碼提交給服務器.
2. 服務器端驗證??名密碼是否正確, 如果正確, 服務器?成令牌, 下發給客?端.
3. 客?端把令牌存儲起來(?如Cookie, local storage等), 后續請求時, 把token發給服務器
4. 服務器對令牌進?校驗, 如果令牌正確, 進?下?步操作
7.Controller
數據準備:
接收與返回的數據:
因為兩個登錄都含有統一數據,所以對公共的進行提取
//共有的字段
@Data
public class UserLoginParam implements Serializable {/*** 身份登錄信息* 可填可不填 不填代表都可以登錄*/private String mandatoryIdentity;
}
UserPasswordLoginParam:
@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam{/*** 手機號或郵箱*/@NotBlank(message = "手機號或郵箱不能為空")private String loginName;/*** 密碼*/@NotBlank(message = "密碼不能為空")private String password;
}
UserMessageLoginParam:
@Data
@EqualsAndHashCode(callSuper = true)
public class UserMessageLoginParam extends UserLoginParam{/*** 登錄手機號*/@NotBlank(message = "手機號不能為空")private String loginMobile;/*** 驗證碼*/@NotBlank(message = "驗證碼不能為空")private String code;
}
UserLoginResult:
@Data
public class UserLoginResult implements Serializable {/*** 令牌*/@NotBlank(message = "令牌不能為空")private String token;/*** 身份信息*/@NotBlank(message = "身份信息不能為空")private String identity;
}
1.密碼登錄:
/*** 密碼登錄* @param userLoginParam* @return*/@RequestMapping("/password/login")private CommonResult<UserLoginResult> passwordLogin(@Validated @RequestBody UserPasswordLoginParam userLoginParam) {logger.info("passwordLogin userLoginParam:{}", JacksonUtil.writeValueAsString(userLoginParam));//使用同一個接口完成登錄UserLoginDTO userLoginDTO = userService.login(userLoginParam);return CommonResult.success(convertToLoginResult(userLoginDTO));}
在數據庫返回的對象使用DTO,然后返回時用convertToLoginResult進行類型轉化。
private UserLoginResult convertToLoginResult(UserLoginDTO userLoginDTO) {//校驗if(userLoginDTO == null) {throw new ControllerException(ControllerErrorCodeConstant.LOGIN_ERROR);}//數據轉化UserLoginResult userLoginResult = new UserLoginResult();userLoginResult.setToken(userLoginDTO.getToken());userLoginResult.setIdentity(userLoginDTO.getIdentity().name());return userLoginResult;}
2.短信驗證碼登錄
與之類似:
/*** 短信驗證碼登錄* @param userLoginParam* @return*/@RequestMapping("/message/login")private CommonResult<UserLoginResult> messageLogin(@Validated @RequestBody UserMessageLoginParam userLoginParam) {logger.info("messageLogin userLoginParam:{}", JacksonUtil.writeValueAsString(userLoginParam));//使用同一個接口完成登錄 將接收參數改為公用參數extendsUserLoginDTO userLoginDTO = userService.login(userLoginParam);return CommonResult.success(convertToLoginResult(userLoginDTO));}
8.Sevice
通過Java14特性屬性校驗并賦值實現一個接口完成兩個登錄功能
/*** 用戶登錄* 1.手機號/郵箱 + 密碼* 2.手機號 + 驗證碼* @param userLoginParam* @return*/@Overridepublic UserLoginDTO login(UserLoginParam userLoginParam) {UserLoginDTO userLoginDTO = null;//類型檢查與類型交換 java 14 版本及以上 實現校驗兩個登錄方式if(userLoginParam instanceof UserPasswordLoginParam loginParam) {//手機號/郵箱 + 密碼userLoginDTO = loginByPassword(loginParam);}else if(userLoginParam instanceof UserMessageLoginParam loginParam) {//手機號 + 驗證碼userLoginDTO = loginByShortMessage(loginParam);}else {throw new ServiceException(ServiceErrorCodeConstant.LOGIN_INFO_NOT_EXITS);}return userLoginDTO;}
DTO:
@Data
public class UserLoginDTO implements Serializable {/*** JWT令牌*/private String token;/*** 身份信息*/private UserIdentityEnum identity;
}
1.通過手機短信登錄
1.校驗手機號
if(!StringUtils.hasText(loginParam.getLoginMobile())) {throw new ServiceException(ServiceErrorCodeConstant.PHONE_NUMBER_ERROR);}
2.通過手機號完成數據庫查詢
UserDO userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginMobile()));
3.校驗數據庫信息及身份(判斷是否是管理員)
//校驗數據庫數據if(userDO == null) {throw new ServiceException(ServiceErrorCodeConstant.USER_INFO_IS_EMPTY);}else if(StringUtils.hasText(loginParam.getMandatoryIdentity())&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {//身份校驗不通過throw new ServiceException(ServiceErrorCodeConstant.IDENTITY_ERROR);}
4.獲取Redis中的驗證碼并于數據庫中的數據進行校驗?
String code = verificationCodeService.getVerificationCode(loginParam.getLoginMobile());if(!code.equals(loginParam.getCode())) {throw new ServiceException(ServiceErrorCodeConstant.VERIFICATION_CODE_ERROR);}
5.將數據封裝成一個token
//塞入返回值(JWT)Map<String, Object> claim = new HashMap<>();claim.put("id", userDO.getId());claim.put("identity", userDO.getIdentity());String token = JWTUtil.getJwt(claim);
6.封裝成為DTO返回
UserLoginDTO userLoginDTO = new UserLoginDTO();userLoginDTO.setToken(token);userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));return userLoginDTO;
2.通過手機號/郵箱+密碼登錄
1.校驗密碼
2.判斷是手機號還是郵箱
3.通過手機號/郵箱獲取數據庫信息
4.校驗 數據庫信息及身份信息
5.生成token
6.包裝成DTO返回
/*** 通過手機號+密碼登錄* @param loginParam* @return*/private UserLoginDTO loginByPassword(UserPasswordLoginParam loginParam) {UserDO userDO = null;if(!StringUtils.hasText(loginParam.getPassword())) {throw new ServiceException(ServiceErrorCodeConstant.PASSWORD_EMPTY);}//判斷是手機號還是郵箱登錄if(RegexUtil.checkMobile(loginParam.getLoginName())) {//手機號//根據手機號查詢用戶表userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginName()));}else if(RegexUtil.checkMail(loginParam.getLoginName())) {//郵箱登錄//根據郵箱查詢用戶表userDO = userMapper.selectByEmail(loginParam.getLoginName());}else {throw new ServiceException(ServiceErrorCodeConstant.LOGIN_NOT_EXITS);}//校驗登錄信息if(userDO == null) {throw new ServiceException(ServiceErrorCodeConstant.USER_INFO_IS_EMPTY);}else if(StringUtils.hasText(loginParam.getMandatoryIdentity())&& !loginParam.getMandatoryIdentity().equalsIgnoreCase(userDO.getIdentity())) {//身份校驗不通過throw new ServiceException(ServiceErrorCodeConstant.IDENTITY_ERROR);}else if(!DigestUtil.sha256Hex(loginParam.getPassword()).equals(userDO.getPassword())) {throw new ServiceException(ServiceErrorCodeConstant.PASSWORD_ERROR);}//塞入返回值(JWT)Map<String, Object> claim = new HashMap<>();claim.put("id", userDO.getId());claim.put("identity", userDO.getIdentity());String token = JWTUtil.getJwt(claim);UserLoginDTO userLoginDTO = new UserLoginDTO();userLoginDTO.setToken(token);userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));return userLoginDTO;}