SpringBoot+SpringSecurity+Redis+jwt實現(xiàn)前后端分離攜帶驗證碼(實戰(zhàn))
寫在前面的話
小白記錄第一次整合Springboot+Springsecurity+hutool+redis+jwt。
本文章使用了大量hutool的工具類,請悉知。
本文章中心是攜帶驗證碼+賬號密碼請求后端驗證,使用Redis存儲驗證碼。
默認(rèn)在閱讀本篇文章的朋友們對Springboot、SpringSecurity、redis、jwt已有認(rèn)知。
因為本項目屬于本人的一個練手項目,所以包含的一些pom依賴如不需要請自行剝離,僅需SpringSecurity、redis、jwt即可。
如有疑問請評論區(qū)友好交流指點。
后續(xù)1(2021-12-05)
在寫本項目的時候,我發(fā)現(xiàn)退出登錄后,雖然SpringSecurity已經(jīng)刪除了憑證,但是jwt還是可以憑借token訪問,這顯然是不太完美的。結(jié)合百度,我想到了使用redis來存儲token,實現(xiàn)退出登錄后及時刪除token。請滑到文檔底部查看最新改動代碼。
后續(xù)2(2021-12-07)
看到評論區(qū)朋友問有沒有g(shù)it地址,剛才上線脫敏了下配置,這就把地址開源出來了。
后端:e.coding.net/yueranzs/vu…
這個練手項目叫做新冠物資管理系統(tǒng),b站看了視頻,結(jié)合自己技術(shù)把前端后端都做了大變動。
前端:e.coding.net/yueranzs/vu…
后端目前圖片存儲使用到的技術(shù)是阿里云OSS(自封裝OSSUtil),大家可以自行復(fù)制。有疑問或補充請評論區(qū)友好交流。
因為本身自己是個后端程序猿所以前端很多注釋,應(yīng)該是易懂的。
orz,不過都還沒有做完,正在一步步學(xué)習(xí)中,前端架構(gòu)是拜托了一個大佬教我學(xué)習(xí)更改的,更容易擴展,大家如想學(xué)習(xí),可以down下來運行試試看。前端我可能沒做脫敏數(shù)據(jù),目前也在學(xué)習(xí)怎么把vue部署在自己的群暉NAS虛擬機上,最終結(jié)果出來我將直接公開域名,供大家訪問。
引入相關(guān)依賴
1.8
UTF-8
UTF-8
2.3.7.RELEASE
1.2.4
5.7.16
5.1.0
3.4.1
2.2
0.11.2
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-devtools
runtime
true
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-aop
com.alibaba
druid-spring-boot-starter
${druid.version}
cn.hutool
hutool-all
${hutool.version}
org.apache.poi
poi
${poi.version}
org.apache.poi
poi-ooxml
${poi.version}
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
com.baomidou
mybatis-plus-generator
${mybatis-plus.version}
org.apache.velocity
velocity-engine-core
${mybatis-plus-velocity.version}
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt-api
${jwt.version}
io.jsonwebtoken
jjwt-impl
${jwt.version}
runtime
io.jsonwebtoken
jjwt-jackson
${jwt.version}
runtime
復(fù)制代碼驗證碼存在redis中,并返回base64到前端
本文章使用的是redis存儲驗證碼,具體組成為key:code_xxx,value:英文數(shù)字(四位)
驗證碼圖返回的base64使用的是hutool工具類(強烈推薦)
LoginController
//randomCode是一個時間戳,由前端生成后請求后端,具體是防止redis中的key重復(fù)
@ApiOperation(value = "驗證碼",notes = "獲取驗證碼")
@GetMapping("/getRandomCode")
public Result getRandomCode(@RequestParam String randomCode){
if (ObjectUtil.isEmpty(randomCode)) {
return Result.error(500,"請輸入驗證碼!");
}
return Result.successData(loginService.getRandomCode(randomCode));
}
復(fù)制代碼LoginServiceImpl
/**
* 獲取驗證碼Base64
*
* @param randomCode
* @return
*/
@Transactional(propagation = Propagation.SUPPORTS,rollbackFor = Exception.class)
@Override
public String getRandomCode(String randomCode) {
//定義圖形驗證碼的長、寬、驗證碼字符數(shù)、干擾元素個數(shù)
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(90, 34, 4, 3);
//設(shè)置背景顏色
captcha.setBackground(Color.WHITE);
//驗證圖形驗證碼的有效性,返回boolean值
captcha.verify("60");
//將字符長存入redis,并判斷redis中是否存在
//RedisUtil,我一會貼在下面
//TimeUnit是個枚舉類,我這里選擇是以秒計時,如60秒后過期清除當(dāng)前驗證碼
boolean redisCode = RedisUtil.set("code_" + randomCode, captcha.getCode(), 過期時長, TimeUnit.SECONDS);
//如果存入redis中失敗,拋出異常
//這里是自定義異常類,可以自行處理,不影響
if (!redisCode) {
new BusinessException(狀態(tài)碼, 返回提示信息);
}
//3.這里只返回Base64字符串用來展示
return captcha.getImageBase64Data();
}
復(fù)制代碼RedisUtil工具類
/**
* 掘金里無法導(dǎo)入整個redis工具類,我這里挑了幾個需要的方法,僅供參考
* redis工具類
* @author yueranzs
* @date 2021-03-04 10:08
*/
public class RedisUtil {
//因為普通類無法直接使用RedisTemplate,這里用hutool中的SpringUtil來獲取bean
//如沒引入hutool的可以百度下springboot中java普通類怎么調(diào)用mapper或service中的接口
//關(guān)鍵注解@Component、@PostConstruct
private static final RedisTemplate redisTemplate = SpringUtil.getBean("redisTemplate");
/**
* 普通緩存放入
*
* @param key 鍵
* @param value 值
* @return true成功 false失敗
*/
public static boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 刪除緩存
*
* @param key 可以傳一個值 或多個
*/
@SuppressWarnings("unchecked")
public static void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
}
復(fù)制代碼異常類
/**
* @author yueranzs
* @date 2021-11-03 17:55
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BusinessException extends RuntimeException{
@ApiModelProperty(value = "狀態(tài)碼")
private Integer code;
@ApiModelProperty(value = "錯誤信息")
private String errMsg;
}
復(fù)制代碼全局異常處理
/**
* 全局異常處理
* @author yueranzs
* @date 2021-11-01 11:55
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 這里的意思是,只要捕獲到BusinessException異常,那么就執(zhí)行此方法
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public Result error(BusinessException exception){
log.error(exception.getErrMsg());
return Result.error(exception.getCode(), exception.getErrMsg());
}
}
復(fù)制代碼封裝返回類
/**
* 封裝返回類
* @author yueranzs
* @date 2021-11-01 10:51
*/
@Data
public class Result {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "響應(yīng)碼")
private Integer code;
@ApiModelProperty(value = "提示信息")
private String message;
@ApiModelProperty(value = "返回數(shù)據(jù)")
private Object data;
/**
* 構(gòu)造方法私有化,里面的方法都是靜態(tài)方法
* 達(dá)到保護屬性的作用
*/
private Result(){
}
/**
* 這里使用鏈?zhǔn)骄幊?br> * @return
*/
public static Result ok(){
Result result = new Result();
result.setSuccess(true);
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(ResultCode.SUCCESS.getMessage());
return result;
}
public static Result ok(Integer code,String message){
Result result = new Result();
result.setSuccess(true);
result.setCode(code);
result.setMessage(message);
return result;
}
public static Result error(){
Result result = new Result();
result.setSuccess(false);
//失敗code
result.setCode(ResultCode.COMMON_FAIL.getCode());
//失敗message
result.setMessage(ResultCode.COMMON_FAIL.getMessage());
return result;
}
public static Result error(Integer code,String message){
Result result = new Result();
result.setSuccess(false);
result.setCode(code);
result.setMessage(message);
return result;
}
public static Result successData(Object data){
Result result = new Result();
result.setSuccess(true);
//成功code
result.setCode(ResultCode.SUCCESS.getCode());
//成功message
result.setMessage(ResultCode.SUCCESS.getMessage());
result.setData(data);
return result;
}
public static Result errorData(Object data){
Result result = new Result();
result.setSuccess(false);
//失敗code
result.setCode(ResultCode.COMMON_FAIL.getCode());
//失敗message
result.setMessage(ResultCode.COMMON_FAIL.getMessage());
result.setData(data);
return result;
}
/**
* 自定義
* @param success
* @return
*/
public Result success(Boolean success){
this.setSuccess(success);
return this;
}
public Result message(String message){
this.setMessage(message);
return this;
}
public Result code(Integer code){
this.setCode(code);
return this;
}
public Result data(Object data){
this.setData(data);
return this;
}
}
復(fù)制代碼訪問獲取驗證碼接口
postman請求返回的數(shù)據(jù)和結(jié)構(gòu)

前端頁面展示情況

redis中存儲情況

編寫SecurityConfig配置類
關(guān)于EnableGlobalMethodSecurity
當(dāng)我們想要開啟spring方法級安全時,只需要在任何 @Configuration 實例上使用 @EnableGlobalMethodSecurity 注解就能達(dá)到此目的。同時這個注解為我們提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三種不同的機制來實現(xiàn)同一種功能。
具體請訪問鏈接,有詳細(xì)解釋:blog.csdn.net/chihaihai/a…
/**
* @author yueranzs
* @date 2021/11/22 13:56
*/
@Configuration
//開啟springsecurity
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginAuthenticationProvider loginAuthenticationProvider;
@Autowired
private LoginUserDetails loginUserDetails;
@Autowired
private AuthenticationDetailsSource authenticationDetailsSource;
/**
* SpringSecurity5.X要求必須指定密碼加密方式,否則會在請求認(rèn)證的時候報錯
* 同樣的,如果指定了加密方式,就必須您的密碼在數(shù)據(jù)庫中存儲的是加密后的,才能比對成功
* @return
*/
@Bean
protected BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入自定義jwttoken過濾器
*/
@Bean
protected JwtAuthenticationTokenFilter authenticationTokenFilter() throws Exception{
return new JwtAuthenticationTokenFilter();
}
/**
* 角色繼承,比如在一個系統(tǒng)中admin屬于最高角色"超級管理員",那么他將擁有其他角色所有的權(quán)限
* 以>來設(shè)置
* admin > user > normal > ......
* @return
*/
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
/**
* 靜態(tài)資源放行
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/js/**", "/css/**","/images/**");
}
/**
* Springsecurity默認(rèn)不攜帶驗證碼進(jìn)行驗證,所以這里我們需要重寫相關(guān)配置類,一會請看代碼
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//將自定義的Provider裝配到Builder
auth.authenticationProvider(loginAuthenticationProvider);
//將自定義的loginserviceimpl裝配到builder
auth.userDetailsService(loginUserDetails).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(rawPassword.toString());
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//Springsecurity放行規(guī)則,permitAll是針對所有方法。
//目使用了swagger,所以需要將swagger相關(guān)的url放行。
//SpringseCurity的放行規(guī)則由上往下,如果前者已被攔截,
//不再執(zhí)行,所以這就是為什么.anyRequest().authenticated()需要放在最后的原因。
http.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/*").permitAll()
.antMatchers("/login/**").permitAll()
//剩下方法攔截
.anyRequest().authenticated()
.and()
.formLogin()
//登錄頁
.loginPage("/login.html")
//登錄請求接口,如果url為空也會默認(rèn)將loginPage的值賦值給url
//可能習(xí)慣性會認(rèn)為需要自己寫一個/login/loginUser的接口,
//但其實這里是交給SpringSecurity自己去檢驗的,默認(rèn)情況下只需要攜帶form-data類型的賬號密碼提交即可。
//本項目將會對默認(rèn)請求進(jìn)行重寫,使用存在redis中的驗證碼驗證
.loginProcessingUrl("/login/loginUser")
//設(shè)置登錄參數(shù)別名
//SpringSecurity默認(rèn)情況下賬號和密碼的屬性名為username、password。
//當(dāng)然也可以跟我一樣重新設(shè)置別名。(雖然設(shè)置的是一樣的,orz)
.usernameParameter("username")
.passwordParameter("password")
//登錄成功后的回調(diào),我看其他博客寫的是自定義返回類,因為我并沒做其他操作,就簡單一點吧,看后面代碼。
//為什么是HttpResponseResult::loginSuccess而其他的卻是->?
//因為登錄成功接口我的形參和該方法形參一致,所以可以這樣寫
.successHandler(HttpResponseResult::loginSuccess)
//登錄失敗回調(diào)
.failureHandler((req, resp, e) -> HttpResponseResult.loginError(resp,e))
//權(quán)限不足回調(diào)
.accessDeniedHandler(HttpResponseResult::insufficientPermissions)
//自定義authenticationDetailsSource,目的是為了獲取請求的驗證碼等信息
.authenticationDetailsSource(authenticationDetailsSource)
.permitAll()
.and()
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((req, resp, auth) -> HttpResponseResult.noLogin(resp))
.and()
//設(shè)置無狀態(tài)的連接,即不創(chuàng)建session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
//退出登錄
.logout()
.logoutUrl("/login/logout")
.logoutSuccessHandler((req,resp,auth) ->HttpResponseResult.logout(resp))
.permitAll()
.and()
;
//使用自定義的jwttoken過濾器來進(jìn)行驗證
http.addFilterBefore(authenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
//禁止頁面緩存
http.headers().cacheControl();
}
}
復(fù)制代碼 編寫SpringSecurity的回調(diào)返回類
/**
* 針對返回響應(yīng)的封裝
* @author yueranzs
* @date 2021/11/22 14:13
*/
@Data
public class HttpResponseResult {
/**
* 基礎(chǔ)返回
* @param resp
* @param jsonObject
* @throws IOException
*/
public static void base(HttpServletResponse resp,JSONObject jsonObject) throws IOException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println(jsonObject);
out.flush();
out.close();
}
/**
* 響應(yīng)返回封裝
* @param resp
* @param resultCode
* @return
*/
public static void data(HttpServletResponse resp,CustomizeResultCode resultCode) throws IOException {
JSONObject result = new JSONObject();
result.set("code",resultCode.getCode());
result.set("message",resultCode.getMessage());
base(resp,result);
}
/**
* 暫無憑證或是認(rèn)證失敗
* @param resp
*/
public static void noProof(HttpServletResponse resp) throws IOException {
data(resp,UserResultCode.USER_NOT_PROOF);
}
/**
* 登錄失敗
* @param resp
* @param exception security的認(rèn)證異常
*/
public static void loginError(HttpServletResponse resp,AuthenticationException exception) throws IOException {
if (exception instanceof LockedException) {
//賬戶鎖定
data(resp,UserResultCode.USER_ACCOUNT_LOCKED);
} else if (exception instanceof CredentialsExpiredException) {
//密碼過期
data(resp,UserResultCode.USER_CREDENTIALS_EXPIRED);
} else if (exception instanceof AccountExpiredException) {
//賬戶過期
data(resp,UserResultCode.USER_ACCOUNT_EXPIRED);
} else if (exception instanceof DisabledException) {
//賬戶禁用
data(resp,UserResultCode.USER_ACCOUNT_DISABLE);
} else if (exception instanceof BadCredentialsException) {
//用戶名或者密碼輸入錯誤
data(resp,UserResultCode.USER_LOGIN_ERROR_NO);
}else if (exception instanceof InternalAuthenticationServiceException){
//用戶不存在
data(resp,UserResultCode.USER_ACCOUNT_NOT_EXIST);
}
}
/**
* 退出
* @param resp
*/
public static void logout(HttpServletResponse resp) throws IOException {
data(resp,UserResultCode.USER_LOGOUT_SUCCESS);
}
/**
* 登錄成功
* @param req
* @param resp
* @param auth
*/
public static void loginSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
//生成token
JwtUtil jwtUtil = new JwtUtil();
Map user = new HashMap<>();
user.put("username",auth.getName());
//token我只包含了username,因為在下面自定義jwttoken過濾器里面會查詢角色等信息
String token = jwtUtil.create(user);
base(resp,new JSONObject().set("code",200).set("data",token));
}
/**
* 權(quán)限不足
* @param req
* @param resp
* @param e
*/
public static void insufficientPermissions(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException {
data(resp,UserResultCode.USER_INSUFFICIENT_PERMISSIONS);
}
}
復(fù)制代碼 User類(pojo)
注意,下面用戶表是mybatis-plus生成,如果想通過SpringSecurity驗證,需要實現(xiàn)UserDetails
/**
*
* 用戶表
*
*
* @author yueranzs
* @since 2021-11-04
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_user")
@ApiModel(value="User對象", description="用戶表")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用戶ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "用戶名")
private String username;
@ApiModelProperty(value = "昵稱")
private String nickname;
@ApiModelProperty(value = "郵箱")
private String email;
@ApiModelProperty(value = "頭像")
private String avatar;
@ApiModelProperty(value = "頭像臨時簽名")
@TableField(exist = false)
private String avatarUrl;
@ApiModelProperty(value = "聯(lián)系電話")
private String phoneNumber;
@ApiModelProperty(value = "狀態(tài) 0鎖定 1有效")
private Integer status;
@ApiModelProperty(value = "創(chuàng)建時間")
private Date createTime;
@ApiModelProperty(value = "修改時間")
private Date modifiedTime;
@ApiModelProperty(value = "性別 0男 1女 2保密")
private Integer sex;
@ApiModelProperty(value = "鹽")
private String salt;
@ApiModelProperty(value = "0:超級管理員,1:系統(tǒng)用戶")
private Integer type;
@ApiModelProperty(value = "密碼")
private String password;
@ApiModelProperty(value = "生日")
private Date birth;
@ApiModelProperty(value = "部門id")
private Long departmentId;
@ApiModelProperty(value = "邏輯刪除")
private Integer deleted;
@ApiModelProperty(value = "角色信息")
//mybatis-plus中的注解,即在對數(shù)據(jù)庫操作時忽略本字段
@TableField(exist = false)
private Set extends GrantedAuthority> authorities;
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
復(fù)制代碼編寫LoginAuthenticationDetailsSource類
/**
* 描述:自定義AuthenticationDetailsSource,將HttpServletRequest注入到AuthenticationDetails,使其能獲取到請求中的驗證碼等其他信息
* @author yueranzs
* @date 2021/12/1 9:42
*/
@Component
public class LoginAuthenticationDetailsSource implements AuthenticationDetailsSource {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new LoginWebAuthenticationDetails(request);
}
}
復(fù)制代碼 編寫LoginWebAuthenticationDetails類
/**
* 描述:自定義WebAuthenticationDetails,將驗證碼和用戶名、密碼一同帶入AuthenticationProvider中
* @author yueranzs
* @date 2021/12/1 9:38
*/
public class LoginWebAuthenticationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 6975601077710753878L;
/*驗證碼value*/
private final String code;
/*驗證碼key*/
private final String randomCode;
public LoginWebAuthenticationDetails(HttpServletRequest request) {
super(request);
//這里的code是指驗證碼真實code,即redis中的驗證碼value,可自行修改成自己項目的屬性名
code = request.getParameter("code");
//redis中的驗證碼key,可自行修改成自己項目的屬性名
randomCode = request.getParameter("randomCode");
}
public String getRandomCode() {
return randomCode;
}
public String getCode() {
return code;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append("; code: ").append(this.getCode());
sb.append(super.toString()).append("; randomCode: ").append(this.getRandomCode());
return sb.toString();
}
}
復(fù)制代碼編寫LoginUserDetails類
/**
* @author yueranzs
* @date 2021/12/1 11:38
*/
@Component
public class LoginUserDetails implements UserDetailsService {
@Autowired
private UserService userService;
/**
* 這里是根據(jù)username(賬號)去查詢數(shù)據(jù)庫,然后進(jìn)行檢驗
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//mybatis-plus的語句,意思是查詢單個的用戶根據(jù)用戶名(username)和偽刪除(delflag)來查
User user = userService.getOne(new QueryWrapper().lambda().select(User::getId,User::getUsername, User::getPassword)
.eq(User::getUsername, username)
.eq(User::getDeleted, ResultCode.NODELETE.getCode()));
if (ObjectUtil.isNull(user)) {
//用戶不存在,拋出SpringSecurity異常
throw new InternalAuthenticationServiceException(UserResultCode.USER_ACCOUNT_NOT_EXIST.getMessage());
}
//查詢角色
List roles = userService.getRolesByUserId(user.getId());
Set authorities = new HashSet();
//注意:SpringSecurity授權(quán)分兩種:角色和權(quán)限
//角色授權(quán):在授權(quán)時,前綴必須加上"ROLE_",一般使用AuthorityUtils.commaSeparatedStringToAuthorityList(字符串,用逗號添加多個role)
//AuthorityUtils.commaSeparatedStringToAuthorityList就不需要自己加"ROLE_"了
//權(quán)限授權(quán):不需要加前綴
//后面的hasRole和hasAuthority千萬不要搞錯了,Role是角色,Authority是權(quán)限,我當(dāng)初就是看錯了,找了很久的問題,后面看代碼
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName())));
//千萬要記得查詢到角色信息后記得設(shè)置
user.setAuthorities(authorities);
//返回
return user;
}
}
復(fù)制代碼 編寫LoginAuthenticationProvider類
/**
* 描述:自定義SpringSecurity的認(rèn)證器
* @author yueranzs
* @date 2021/12/1 9:44
*/
@Component
public class LoginAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private LoginUserDetails loginUserDetails;
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
return null;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//用戶名
String username = authentication.getName();
//密碼
String password = authentication.getCredentials().toString();
LoginWebAuthenticationDetails loginWebAuthenticationDetails= (LoginWebAuthenticationDetails)authentication.getDetails();
//驗證碼key
String randomCode = loginWebAuthenticationDetails.getRandomCode();
//驗證碼value
String code = loginWebAuthenticationDetails.getCode();
//驗證碼是否為空
if (ObjectUtil.isEmpty(randomCode) || ObjectUtil.isEmpty(code)) {
throw new NullPointerException("請輸入驗證碼");
}
//檢驗驗證碼是否正確
if (!validateVerifyRandomCode(randomCode,code)) {
throw new BusinessException(UserResultCode.REDIS_CODE.getCode(), UserResultCode.REDIS_CODE.getMessage());
}
User user = (User) loginUserDetails.loadUserByUsername(username);
//密碼是否一致
if (!user.getPassword().equals(SecureUtil.md5(password))) {
//密碼錯誤,不過因為安全性的問題所以返回此異常,意思是用戶名或者密碼錯誤
throw new BadCredentialsException(UserResultCode.USER_CREDENTIALS_ERROR.getMessage());
}
//刪除redis的驗證碼
RedisUtil.del("code_" + randomCode);
return this.createSuccessAuthentication(user,authentication,user);
}
/**
* 驗證用戶輸入的驗證碼
* @param randomCode 驗證碼key
* @param code 驗證碼value
* @return
*/
public boolean validateVerifyRandomCode(String randomCode,String code){
//驗證碼是否一致
Object redisCode = RedisUtil.get("code_" + randomCode);
return ObjectUtil.equals(code, redisCode);
}
}
復(fù)制代碼編寫Jwt配置類
/**
* jwt配置類
* @author yueranzs
* @date 2021/12/4 9:57
*/
@Data
@ToString
@Configuration
//與配置文件中的數(shù)據(jù)關(guān)聯(lián)起來(這個注解會默認(rèn)自動匹配jwt開頭的配置)
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {
/*request Headers : Authorization*/
private String header;
/*Base64對該令牌進(jìn)行編碼*/
private String base64Secret;
/*令牌過期時間 此處單位/毫秒 */
private Long tokenValidityInSeconds;
}
復(fù)制代碼JwtUtil工具類
注意,本工具類建立在hutool工具類的基礎(chǔ)上,僅供參考,部分屬性值請視自己情況定
/**
* jwt工具類
* @author yueranzs
* @date 2021/11/25 15:55
*/
@Component
public class JwtUtil {
private static JwtConfig jwtConfig;
@Autowired
private void setJwtConfig(JwtConfig jwtConfig){
JwtUtil.jwtConfig = jwtConfig;
}
/**
* 生成jwt
* @param payload 數(shù)據(jù)主體
* @return
*/
public String create(Map payload){
//每個jwt都默認(rèn)生成一個到期時間
payload.put("expire_time", DateUtil.current() + jwtConfig.getTokenValidityInSeconds());
//生成私鑰
JWTSigner jwtSigner = JWTSignerUtil.hs256(jwtConfig.getBase64Secret().getBytes(StandardCharsets.UTF_8));
//生成token
return JWTUtil.createToken(payload,jwtSigner);
}
/**
* 解析jwt
* @param token
* @return
*/
public JSONObject parse(String token){
return JWTUtil.parseToken(token).getPayload().getClaimsJson();
}
/**
* 校驗token是否正確
* @param token
* @return
*/
public boolean verifyToken(String token){
//先判斷是否到期,再判斷是否正確
if (expiredToken(token)) {
return JWTUtil.verify(token,jwtConfig.getBase64Secret().getBytes(StandardCharsets.UTF_8));
}
return false;
}
/**
* 校驗token是否過期
* @param token
* @return
*/
public boolean expiredToken(String token){
return DateUtil.current() < getExpiredToken(token);
}
/**
* 獲取token過期時間
* @param token
* @return
*/
public long getExpiredToken(String token){
return Long.parseLong(parse(token).get("expire_time").toString());
}
/**
* 獲取登錄人賬號
* @param token
* @return
*/
public String getUserNameToken(String token){
return parse(token).get("username").toString();
}
/**
* 獲取登錄人角色集合
* @param token
* @return
*/
public Set getRolesToken(String token){
return (Set) parse(token).get("authorities");
}
}
復(fù)制代碼 applicaiton.yml中進(jìn)行追加jwt信息
jwt:
# 請求頭,就是在header中攜帶的令牌名稱,任意名字都可以
header: Authorization
# 鹽值
base64-secret: jwt加密的密鑰,任意填寫
# 過期時間 ,單位/毫秒
token-validity-in-seconds: 過期時間
復(fù)制代碼編寫JwtAuthenticationTokenFilter過濾類
/**
* jwttokenfilter
* @author yueranzs
* @date 2021/12/4 10:14
*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtUtil jwtUtil;
@Resource
private JwtConfig jwtConfig;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUrl = request.getRequestURI();
String authToken = request.getHeader(jwtConfig.getHeader());
String userName = null;
if (ObjectUtil.isNotEmpty(authToken)) {
userName = jwtUtil.getUserNameToken(authToken);
}
log.info("進(jìn)入jwt自定義token過濾器");
log.info("自定義token過濾器獲得用戶名為:" + userName);
//當(dāng)userName不為空時進(jìn)行校驗token是否為有效token
//ObjectUtil.isNotEmpty()和ObjectUtil.isNull()是hutool中的方法。
/*
前者意思是指對象是否不為空,和isNotNull()不同。
比如"",isNotNull()會返回true而isNotEmpty()會返回false。
userName是字符串所以使用isNotEmpty(),該方法也很適合集合判空
*/
/*
getAuthentication()使用isNull()原因是:
通過前面幾個代碼塊的代碼,可以看出是存儲授權(quán)信息的
這里的意思是如果用戶名不為空并且授權(quán)信息又有值,那么就直接跳過,反之就是進(jìn)入下面的if內(nèi)部
*/
if (ObjectUtil.isNotEmpty(userName) && ObjectUtil.isNull(SecurityContextHolder.getContext().getAuthentication())) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userName);
User user = (User) userDetails;
//檢驗token
if (!jwtUtil.verifyToken(authToken)) {
throw new BusinessException(500,"token已過期");
}else if (StrUtil.equals(userName,user.getUsername())){
/**
* UsernamePasswordAuthenticationToken繼承AbstractAuthenticationToken實現(xiàn)Authentication
* 所以當(dāng)在頁面中輸入用戶名和密碼之后首先會進(jìn)入到UsernamePasswordAuthenticationToken驗證(Authentication),
* 然后生成的Authentication會被交由AuthenticationManager來進(jìn)行管理
* 而AuthenticationManager管理一系列的AuthenticationProvider,
* 而每一個Provider都會通UserDetailsService和UserDetail來返回一個
* 以UsernamePasswordAuthenticationToken實現(xiàn)的帶用戶名和密碼以及權(quán)限的Authentication
*/
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//將authentication放入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request,response);
}
}
復(fù)制代碼hasRole、hasAuthority
關(guān)于這兩只的區(qū)別可以看鏈接:Spring Security 中的 hasRole 和 hasAuthority 有區(qū)別嗎? - 云+社區(qū) - 騰訊云 (tencent.com)
/**
* 前面代碼塊中我說過這兩個注解千萬不要混淆,雖然在使用上,都并不需要加前綴
* 但我之前沒注意清楚,在給用戶授權(quán)時我寫了ROLE_admin,但是使用的是hasAuthority
* 也就導(dǎo)致我怎么都訪問不了這個方法,后面半信半疑hasAuthority('ROLE_admin')才能訪問
* 再后來發(fā)現(xiàn)是自己用錯方法了,換上hasRole('admin')就沒問題
*
* @PreAuthorize可以看我第一個分享的鏈接
* hashRole和hasAuthority在springsecurity4的時候才有了ROLE_前綴區(qū)分,早期幾乎是一模一樣的
* @return
*/
@PreAuthorize("hasAuthority('admin')")
@ApiOperation(value = "測試一下",notes = "測試一下")
@GetMapping("/test")
public Result test(){
return Result.successData("hahah");
}
復(fù)制代碼運行效果
登錄成功

登錄失敗

token過期

暫無權(quán)限

ps:其他的一些狀態(tài)碼暫未測試,目前這些也已足以,后續(xù)如有其他需要補充的我會再來添代碼。就先這樣吧。謝謝閱讀。
后續(xù)1-改造代碼(2021-12-05)
調(diào)整SpringSecurity的回調(diào)返回類中l(wèi)oginSuccess()
//加上下面注解,因為需要獲取jwtConfig的過期時間
@Component
public class HttpResponseResult {
//關(guān)于jwtConfig的都是需要新增的代碼
private static JwtConfig jwtConfig;
@Autowired
private void setJwtConfig(JwtConfig jwtConfig){
HttpResponseResult.jwtConfig = jwtConfig;
}
/**
* 登錄成功
* @param req
* @param resp
* @param auth
*/
public static void loginSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication auth) throws IOException {
//生成token
JwtUtil jwtUtil = new JwtUtil();
Map user = new HashMap<>();
user.put("username",auth.getName());
String token = jwtUtil.create(user);
//將token存入redis
//這里的key結(jié)構(gòu)為:"token_" + userName
//token失效時間和jwt失效時間保持一致
//jwtConfig.getTokenValidityInSeconds():獲取jwt失效時間
RedisUtil.set("token_" + auth.getName(), token, jwtConfig.getTokenValidityInSeconds(), TimeUnit.SECONDS);
base(resp,new JSONObject().set("code",200).set("data",token));
}
}
復(fù)制代碼 調(diào)整SecurityConfig部分代碼
將關(guān)于退出登錄的security方法全部刪除,為什么?
本來是想在退出方法里進(jìn)行刪除redis的token,但是因為存儲token的key一部分是獲取當(dāng)前userName,而只要訪問了logout()就默認(rèn)進(jìn)入了security的過濾器,如果想改變的話會比較麻煩,所以我打算自行實現(xiàn),很方便。
只需注釋"http://退出登錄,這里劃重點,我全部注釋了"下面代碼即可。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/*").permitAll()
.antMatchers("/login/getRandomCode","/login/getUserAvatar").permitAll()
// .antMatchers("/admin/**").hasRole("admin")
// .antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and()
.formLogin()
//登錄頁
.loginPage("/login.html")
//登錄請求接口,如果url為空也會默認(rèn)將loginPage的值賦值給url
.loginProcessingUrl("/login/loginUser")
//設(shè)置登錄參數(shù)別名
.usernameParameter("username")
.passwordParameter("password")
.successHandler(HttpResponseResult::loginSuccess)
.failureHandler((req, resp, e) -> HttpResponseResult.loginError(resp,e))
.authenticationDetailsSource(authenticationDetailsSource)
.permitAll()
.and()
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((req, resp, auth) -> HttpResponseResult.noProof(resp))
.accessDeniedHandler(HttpResponseResult::insufficientPermissions)
.and()
//設(shè)置無狀態(tài)的連接,即不創(chuàng)建session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
//退出登錄,這里劃重點,我全部注釋了
/*.logout()
.logoutUrl("/login/logout")
.logoutSuccessHandler(HttpResponseResult::logout)
.permitAll()
.and()*/
;
http.addFilterBefore(authenticationTokenFilter(),UsernamePasswordAuthenticationFilter.class);
//禁止頁面緩存
http.headers().cacheControl();
}
復(fù)制代碼新增UserUtil工具類
方便獲取當(dāng)前登陸人信息
/**
* @author yueranzs
* @date 2021/12/5 12:24
*/
public class UserUtil {
/**
* 獲取當(dāng)前登陸人信息
* @return
*/
public static User getUser(){
return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
/**
* 獲取當(dāng)前登錄人賬號
* @return
*/
public static String getUserName(){
return getUser().getUsername();
}
/**
* 獲取當(dāng)前登錄人編號
* @return
*/
public static Long getUserId(){
return getUser().getId();
}
/**
* 獲取當(dāng)前登錄人角色信息
* @return
*/
public static Set extends GrantedAuthority> getUserRole(){
return getUser().getAuthorities();
}
}
復(fù)制代碼LoginController中新增logout()退出登錄方法
@ApiOperation(value = "退出登錄",notes = "退出登錄")
@GetMapping("/logout")
public JSONObject logout(){
return loginService.logout();
}
復(fù)制代碼LoginService實現(xiàn)類
/**
* 退出登錄
*
* @return
*/
@Override
public JSONObject logout() {
//僅作返回退出登錄的結(jié)果
JSONObject object = new JSONObject();
//查找redis中是否存在此token,如果不為空(isNotEmpty的用法上面說過)就刪除該token
if (ObjectUtil.isNotEmpty(RedisUtil.get("token_" + UserUtil.getUserName()))) {
//清除token,注意順序不要弄反了
//要先清除redis的token才能清除認(rèn)證對象,不然是無法通過UserUtil獲取到userName的
RedisUtil.del("token_" + UserUtil.getUserName());
//只需要清除認(rèn)證對象,因為在springsecurity中并沒有設(shè)置session,所以不需要清空
SecurityContextHolder.getContext().setAuthentication(null);
//退出登錄成功code
object.set("code",UserResultCode.USER_LOGOUT_SUCCESS.getCode());
//退出登錄成功message
object.set("message",UserResultCode.USER_LOGOUT_SUCCESS.getMessage());
return object;
}
//退出登錄失敗code
object.set("code",UserResultCode.USER_LOGOUT_ERROR.getCode());
//退出登錄失敗message
object.set("message",UserResultCode.USER_LOGOUT_ERROR.getMessage());
return object;
}
復(fù)制代碼調(diào)整JwtAuthenticationTokenFilter
/**
* jwttokenfilter
* @author yueranzs
* @date 2021/12/4 10:14
*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtUtil jwtUtil;
@Resource
private JwtConfig jwtConfig;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestUrl = request.getRequestURI();
String authToken = request.getHeader(jwtConfig.getHeader());
String userName = null;
if (ObjectUtil.isNotEmpty(authToken)) {
userName = jwtUtil.getUserNameToken(authToken);
}
log.info("進(jìn)入jwt自定義token過濾器");
log.info("自定義token過濾器獲得用戶名為:" + userName);
//當(dāng)userName不為空時進(jìn)行校驗token是否為有效token
if (ObjectUtil.isNotEmpty(userName) && ObjectUtil.isNull(SecurityContextHolder.getContext().getAuthentication())) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userName);
User user = (User) userDetails;
//檢驗token,新增下面的if判斷
if(ObjectUtil.isEmpty(RedisUtil.get("token_" + userName))){
throw new BusinessException(500,"token已被清除");
} else if (!jwtUtil.verifyToken(authToken)) {
throw new BusinessException(500,"token已過期");
}else if (StrUtil.equals(userName,user.getUsername())) {
/**
* UsernamePasswordAuthenticationToken繼承AbstractAuthenticationToken實現(xiàn)Authentication
* 所以當(dāng)在頁面中輸入用戶名和密碼之后首先會進(jìn)入到UsernamePasswordAuthenticationToken驗證(Authentication),
* 然后生成的Authentication會被交由AuthenticationManager來進(jìn)行管理
* 而AuthenticationManager管理一系列的AuthenticationProvider,
* 而每一個Provider都會通UserDetailsService和UserDetail來返回一個
* 以UsernamePasswordAuthenticationToken實現(xiàn)的帶用戶名和密碼以及權(quán)限的Authentication
*/
//清除密碼
user.setPassword(null);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//將authentication放入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request,response);
}
}
復(fù)制代碼運行結(jié)果


后面的話
那么就先補充到這里吧,等后續(xù)還缺漏什么的我會繼續(xù)更新,謝謝觀看。
作者:yueranzs
鏈接:https://juejin.cn/post/7037792636807151630
來源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
