springboot前后端分離項(xiàng)目集成Security框架和JWT完成用戶校驗(yàn)

前言

Java流行的安全框架有兩種Apache Shiro和Spring Security饮六,其中Shiro對(duì)于前后端分離項(xiàng)目不是很友好,最終選用了Spring Security。SpringBoot提供了官方的spring-boot-starter-security,能夠方便的集成到SpringBoot項(xiàng)目中,但是企業(yè)級(jí)的使用上,還是需要稍微改造下塞耕,本文實(shí)現(xiàn)了如下功能:

匿名用戶訪問無權(quán)限資源時(shí)的異常處理

登錄用戶是否有權(quán)限訪問資源

集成JWT實(shí)現(xiàn)登陸授權(quán)訪問

token過期處理

自定義用戶登陸邏輯

SpringSecurity簡介

Spring Security是一個(gè)功能強(qiáng)大且高度可定制的身份驗(yàn)證和訪問控制框架。它實(shí)際上是保護(hù)基于sprin的應(yīng)用程序的標(biāo)準(zhǔn)嘴瓤。

Spring Security是一個(gè)框架扫外,側(cè)重于為Java應(yīng)用程序提供身份驗(yàn)證和授權(quán)。與所有Spring項(xiàng)目一樣廓脆,Spring安全性的真正強(qiáng)大之處在于它可以輕松地?cái)U(kuò)展以滿足定制需求

Spring Security 是針對(duì)Spring項(xiàng)目的安全框架筛谚,也是Spring Boot底層安全模塊默認(rèn)的技術(shù)選型,他可以實(shí)現(xiàn)強(qiáng)大的Web安全控制停忿,對(duì)于安全控制驾讲,我們僅需要引入 spring-boot-starter-security 模塊,進(jìn)行少量的配置,即可實(shí)現(xiàn)強(qiáng)大的安全管理

1. 引入必要依賴

<!--spring security-->

<dependency>

? <groupId>org.springframework.boot</groupId>

? <artifactId>spring-boot-starter-security</artifactId>

</dependency>

<!-- redis 緩存操作 -->

<dependency>

? <groupId>org.springframework.boot</groupId>

? <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

spring-boot-starter-security用于集成security, spring-boot-starter-data-redis用于實(shí)現(xiàn)redis緩存

2. 實(shí)現(xiàn)AuthenticationEntryPoint

實(shí)現(xiàn)AuthenticationEntryPoint吮铭,控制用戶無權(quán)限時(shí)的操作

/**

* 認(rèn)證失敗處理類 返回未授權(quán)

*

* @author fanglei

* @Date 2023年7月31日17:54:39

*/

@Component

? ? public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable

{

? ? private static final long serialVersionUID = -8970718410437077606L;

? ? @Override

? ? public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)

? ? throws IOException

? ? {

? ? ? ? int code = HttpStatus.UNAUTHORIZED;

? ? ? ? String msg = StringUtils.format("請(qǐng)求訪問:{}时迫,認(rèn)證失敗,請(qǐng)聯(lián)系管理員", request.getRequestURI());

? ? ? ? ServletUtils.renderString(response, JSON.toJSONString(ApiResult.error(code, msg)));

? ? }

}

3. 實(shí)現(xiàn)UserDetailsService

實(shí)現(xiàn)了UserDetailsService,判斷用戶是否存在,完成用戶自定義登陸

/**

* 用戶驗(yàn)證處理

*

* @author fanglei

*/

@Service

@Slf4j

public class UserDetailsServiceImpl implements UserDetailsService

{

? ? private static final String isEnable ="1";

? ? @Autowired

? ? private SysUserService userService;

? ? @Override

? ? public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException

? ? {

? ? ? ? SysUser user = userService.getBaseMapper().selectOne(new QueryWrapper<SysUser>().eq("user_name", username));

? ? ? ? if (StringUtils.isNull(user))

? ? ? ? {

? ? ? ? ? ? log.info("登錄用戶:{} 不存在.", username);

? ? ? ? ? ? throw new BaseException("登錄用戶:" + username + " 不存在");

? ? ? ? }

? ? ? ? else if (isEnable.equals(user.getIsDel()))

? ? ? ? {

? ? ? ? ? ? log.info("登錄用戶:{} 已被刪除.", username);

? ? ? ? ? ? throw new BaseException("對(duì)不起谓晌,您的賬號(hào):" + username + " 已被刪除");

? ? ? ? }

? ? ? ? else if (isEnable.equals(user.getStatus()))

? ? ? ? {

? ? ? ? ? ? log.info("登錄用戶:{} 已被停用.", username);

? ? ? ? ? ? throw new BaseException("對(duì)不起掠拳,您的賬號(hào):" + username + " 已停用");

? ? ? ? }

? ? ? ? return new LoginUser(user.getUserId(), user,null);

? ? }

}

/**

* @author 方磊

*/

@Data

public class LoginUser implements UserDetails {

? ? private static final long serialVersionUID = 1L;

? ? /**

? ? * 用戶ID

? ? */

? ? private Long userId;

? ? /**

? ? * 租戶id

? ? */

? ? private Long tenantId;

? ? /**

? ? * 用戶唯一標(biāo)識(shí)

? ? */

? ? private String token;

? ? /**

? ? * 登錄時(shí)間

? ? */

? ? private Long loginTime;

? ? /**

? ? * 過期時(shí)間

? ? */

? ? private Long expireTime;

? ? /**

? ? * 登錄IP地址

? ? */

? ? private String ipaddress;

? ? /**

? ? * 登錄地點(diǎn)

? ? */

? ? private String loginLocation;

? ? /**

? ? * 權(quán)限列表

? ? */

? ? private Set<String> permissions;

? ? /**

? ? * 用戶信息

? ? */

? ? private SysUser user;

? ? public LoginUser(SysUser user, Set<String> permissions)

? ? {

? ? ? ? this.user = user;

? ? ? ? this.permissions = permissions;

? ? }

? ? public LoginUser(Long userId, SysUser user, Set<String> permissions)

? ? {

? ? ? ? this.userId = userId;

? ? ? ? this.user = user;

? ? ? ? this.permissions = permissions;

? ? }

? ? @Override

? ? public Collection<? extends GrantedAuthority> getAuthorities() {

? ? ? ? return null;

? ? }

? ? @JSONField(serialize = false)

? ? @Override

? ? public String getPassword()

? ? {

? ? ? ? return user.getPassword();

? ? }

? ? @Override

? ? public String getUsername()

? ? {

? ? ? ? return user.getUserName();

? ? }

? ? /**

? ? * 賬戶是否未過期,過期無法驗(yàn)證

? ? */

? ? @JSONField(serialize = false)

? ? @Override

? ? public boolean isAccountNonExpired()

? ? {

? ? ? ? return true;

? ? }

? ? /**

? ? * 指定用戶是否解鎖,鎖定的用戶無法進(jìn)行身份驗(yàn)證

? ? *

? ? * @return

? ? */

? ? @JSONField(serialize = false)

? ? @Override

? ? public boolean isAccountNonLocked()

? ? {

? ? ? ? return true;

? ? }

? ? /**

? ? * 指示是否已過期的用戶的憑據(jù)(密碼),過期的憑據(jù)防止認(rèn)證

? ? *

? ? * @return

? ? */

? ? @JSONField(serialize = false)

? ? @Override

? ? public boolean isCredentialsNonExpired()

? ? {

? ? ? ? return true;

? ? }

? ? /**

? ? * 是否可用 ,禁用的用戶不能身份驗(yàn)證

? ? *

? ? * @return

? ? */

? ? @JSONField(serialize = false)

? ? @Override

? ? public boolean isEnabled()

? ? {

? ? ? ? return true;

? ? }

}

4. 退出登陸處理

退出登陸:刪除用戶緩存紀(jì)錄并且發(fā)送給前端

/**

* 自定義退出處理類 返回成功

*

* @author fanglei

*/

@Configuration

public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler

{

? ? @Autowired

? ? private TokenService tokenService;

? ? /**

? ? * 退出處理

? ? *

? ? * @return

? ? */

? ? @Override

? ? public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)

? ? ? ? ? ? throws IOException, ServletException

? ? {

? ? ? ? LoginUser loginUser = tokenService.getLoginUser(request);

? ? ? ? if (StringUtils.isNotNull(loginUser))

? ? ? ? {

? ? ? ? ? ? String userName = loginUser.getUsername();

? ? ? ? ? ? // 刪除用戶緩存記錄

? ? ? ? ? ? tokenService.delLoginUser(loginUser.getToken());

? ? ? ? }

? ? ? ? ServletUtils.renderString(response, JSON.toJSONString(ApiResult.success("退出成功")));

? ? }

}

5. 配置security配置

/**

* spring security配置

*

* @author fanglei

*/

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

public class SecurityConfig extends WebSecurityConfigurerAdapter

{

? ? /**

? ? * 自定義用戶認(rèn)證邏輯

? ? */

? ? @Autowired

? ? private UserDetailsService userDetailsService;

? ? /**

? ? * 認(rèn)證失敗處理類

? ? */

? ? @Autowired

? ? private AuthenticationEntryPointImpl unauthorizedHandler;

? ? /**

? ? * 退出處理類

? ? */

? ? @Autowired

? ? private LogoutSuccessHandlerImpl logoutSuccessHandler;

? ? /**

? ? * token認(rèn)證過濾器

? ? */

? ? @Autowired

? ? private JwtAuthenticationTokenFilter authenticationTokenFilter;

? ? /**

? ? * 跨域過濾器

? ? */

? ? @Autowired

? ? private CorsFilter corsFilter;

? ? /**

? ? * 允許匿名訪問的地址

? ? */

? ? @Autowired

? ? private PermitAllUrlProperties permitAllUrl;

? ? /**

? ? * 解決 無法直接注入 AuthenticationManager

? ? *

? ? * @return

? ? * @throws Exception

? ? */

? ? @Bean

? ? @Override

? ? public AuthenticationManager authenticationManagerBean() throws Exception

? ? {

? ? ? ? return super.authenticationManagerBean();

? ? }

? ? /**

? ? * anyRequest? ? ? ? ? |? 匹配所有請(qǐng)求路徑

? ? * access? ? ? ? ? ? ? |? SpringEl表達(dá)式結(jié)果為true時(shí)可以訪問

? ? * anonymous? ? ? ? ? |? 匿名可以訪問

? ? * denyAll? ? ? ? ? ? |? 用戶不能訪問

? ? * fullyAuthenticated? |? 用戶完全認(rèn)證可以訪問(非remember-me下自動(dòng)登錄)

? ? * hasAnyAuthority? ? |? 如果有參數(shù),參數(shù)表示權(quán)限纸肉,則其中任何一個(gè)權(quán)限可以訪問

? ? * hasAnyRole? ? ? ? ? |? 如果有參數(shù)溺欧,參數(shù)表示角色,則其中任何一個(gè)角色可以訪問

? ? * hasAuthority? ? ? ? |? 如果有參數(shù)毁靶,參數(shù)表示權(quán)限胧奔,則其權(quán)限可以訪問

? ? * hasIpAddress? ? ? ? |? 如果有參數(shù),參數(shù)表示IP地址预吆,如果用戶IP和參數(shù)匹配,則可以訪問

? ? * hasRole? ? ? ? ? ? |? 如果有參數(shù)胳泉,參數(shù)表示角色拐叉,則其角色可以訪問

? ? * permitAll? ? ? ? ? |? 用戶可以任意訪問

? ? * rememberMe? ? ? ? ? |? 允許通過remember-me登錄的用戶訪問

? ? * authenticated? ? ? |? 用戶登錄后可訪問

? ? */

? ? @Override

? ? protected void configure(HttpSecurity httpSecurity) throws Exception

? ? {

? ? ? ? // 注解標(biāo)記允許匿名訪問的url

? ? ? ? ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();

? ? ? ? permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());

? ? ? ? httpSecurity

? ? ? ? ? ? ? ? // CSRF禁用,因?yàn)椴皇褂胹ession

? ? ? ? ? ? ? ? .csrf().disable()

? ? ? ? ? ? ? ? // 禁用HTTP響應(yīng)標(biāo)頭

? ? ? ? ? ? ? ? .headers().cacheControl().disable().and()

? ? ? ? ? ? ? ? // 認(rèn)證失敗處理類

? ? ? ? ? ? ? ? .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

? ? ? ? ? ? ? ? // 基于token扇商,所以不需要session

? ? ? ? ? ? ? ? .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

? ? ? ? ? ? ? ? // 過濾請(qǐng)求

? ? ? ? ? ? ? ? .authorizeRequests()

? ? ? ? ? ? ? ? // 對(duì)于登錄login 注冊(cè)register 驗(yàn)證碼captchaImage 允許匿名訪問

? ? ? ? ? ? ? ? .antMatchers("/login", "/register", "/captchaImage").permitAll()

? ? ? ? ? ? ? ? // 除上面外的所有請(qǐng)求全部需要鑒權(quán)認(rèn)證

? ? ? ? ? ? ? ? .anyRequest().authenticated()

? ? ? ? ? ? ? ? .and()

? ? ? ? ? ? ? ? .headers().frameOptions().disable();

? ? ? ? // 添加Logout filter

? ? ? ? httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);

? ? ? ? // 添加JWT filter

? ? ? ? httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

? ? ? ? // 添加CORS filter

? ? ? ? httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);

? ? ? ? httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);

? ? }

? ? /**

? ? * 強(qiáng)散列哈希加密實(shí)現(xiàn)

? ? */

? ? @Bean

? ? public BCryptPasswordEncoder bCryptPasswordEncoder()

? ? {

? ? ? ? return new BCryptPasswordEncoder();

? ? }

? ? /**

? ? * 身份認(rèn)證接口

? ? */

? ? @Override

? ? protected void configure(AuthenticationManagerBuilder auth) throws Exception

? ? {

? ? ? ? auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());

? ? }

}

6. 生成token

? ? /**

? ? * 創(chuàng)建令牌

? ? *

? ? * @param loginUser 用戶信息

? ? * @return 令牌

? ? */

? ? public String createToken(LoginUser loginUser)

? ? {

? ? ? ? String token = IdUtils.fastUUID();

? ? ? ? loginUser.setToken(token);

? ? ? ? setUserAgent(loginUser);

? ? ? ? refreshToken(loginUser);

? ? ? ? Map<String, Object> claims = new HashMap<>();

? ? ? ? claims.put(Constants.LOGIN_USER_KEY, token);

? ? ? ? return createToken(claims);

? ? }

? ? /**

? ? * 從數(shù)據(jù)聲明生成令牌

? ? *

? ? * @param claims 數(shù)據(jù)聲明

? ? * @return 令牌

? ? */

? ? private String createToken(Map<String, Object> claims)

? ? {

? ? ? ? String token = Jwts.builder()

? ? ? ? ? ? ? ? .setClaims(claims)

? ? ? ? ? ? ? ? .signWith(SignatureAlgorithm.HS512, secret).compact();

? ? ? ? return token;

? ? }

? ? /**

? ? * 驗(yàn)證令牌有效期凤瘦,相差不足20分鐘,自動(dòng)刷新緩存

? ? *

? ? * @param loginUser

? ? * @return 令牌

? ? */

? ? public void verifyToken(LoginUser loginUser)

? ? {

? ? ? ? long expireTime = loginUser.getExpireTime();

? ? ? ? long currentTime = System.currentTimeMillis();

? ? ? ? if (expireTime - currentTime <= MILLIS_MINUTE_TEN)

? ? ? ? {

? ? ? ? ? ? refreshToken(loginUser);

? ? ? ? }

? ? }

? ? /**

? ? * 刷新令牌有效期

? ? *

? ? * @param loginUser 登錄信息

? ? */

? ? public void refreshToken(LoginUser loginUser)

? ? {

? ? ? ? loginUser.setLoginTime(System.currentTimeMillis());

? ? ? ? loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);

? ? ? ? // 根據(jù)uuid將loginUser緩存

? ? ? ? String userKey = getTokenKey(loginUser.getToken());

? ? ? ? redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);

? ? }

7. 實(shí)現(xiàn)用戶登錄接口

/**

* @author 方磊

*/

@RequestMapping("/api/v1")

@RestController

public class LoginController {

? ? @Autowired

? ? private TokenService tokenService;

? ? @Autowired

? ? private SysUserService userService;


? ? @GetMapping("/login")

? ? public ApiResult login(String userName , String password){

? ? ? ? LoginUser loginUser = sysService.queryUserByUserNmae(userName);

? ? ? ? retrun ApiResult.success(tokenService.createToken(loginUser));

? ? }


}

8. 總結(jié)

自此security簡單集成完成 基礎(chǔ)項(xiàng)目可簡單使用 可以使用token進(jìn)行驗(yàn)證 權(quán)限模塊完善中.....

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末案铺,一起剝皮案震驚了整個(gè)濱河市蔬芥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌控汉,老刑警劉巖笔诵,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異姑子,居然都是意外死亡乎婿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門街佑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谢翎,“玉大人,你說我怎么就攤上這事沐旨∩” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵磁携,是天一觀的道長褒侧。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么璃搜? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任拖吼,我火速辦了婚禮,結(jié)果婚禮上这吻,老公的妹妹穿的比我還像新娘吊档。我一直安慰自己,他們只是感情好唾糯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布怠硼。 她就那樣靜靜地躺著,像睡著了一般移怯。 火紅的嫁衣襯著肌膚如雪香璃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天舟误,我揣著相機(jī)與錄音葡秒,去河邊找鬼。 笑死嵌溢,一個(gè)胖子當(dāng)著我的面吹牛眯牧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赖草,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼学少,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了秧骑?” 一聲冷哼從身側(cè)響起版确,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乎折,沒想到半個(gè)月后绒疗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡笆檀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年忌堂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酗洒。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡士修,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出樱衷,到底是詐尸還是另有隱情棋嘲,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布矩桂,位于F島的核電站沸移,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜雹锣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一网沾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蕊爵,春花似錦辉哥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至会放,卻和暖如春饲齐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背咧最。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國打工捂人, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窗市。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓先慷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親咨察。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容