前言
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)限模塊完善中.....