Spring Security單項(xiàng)目權(quán)限設(shè)計(jì)過(guò)程解析
為什么選擇SpringSecurity?
現(xiàn)如今,在JavaWeb的世界里Spring可以說(shuō)是一統(tǒng)江湖,隨著微服務(wù)的到來(lái),SpringCloud可以說(shuō)是Java程序員必須熟悉的框架,就連阿里都為SpringCloud寫開(kāi)源呢。(比如大名鼎鼎的Nacos)作為Spring的親兒子,SpringSecurity很好的適應(yīng)了了微服務(wù)的生態(tài)。你可以非常簡(jiǎn)便的結(jié)合Oauth做認(rèn)證中心服務(wù)。本文先從最簡(jiǎn)單的單體項(xiàng)目開(kāi)始,逐步掌握Security。更多可達(dá)官方文檔
準(zhǔn)備
我準(zhǔn)備了一個(gè)簡(jiǎn)單的demo,具體代碼會(huì)放到文末。提前聲明,本demo沒(méi)有用JWT,因?yàn)槲蚁氚裻oken的維護(hù)放到服務(wù)端,更好的維護(hù)過(guò)期時(shí)間。(當(dāng)然,如果將來(lái)微服務(wù)認(rèn)證中心的形式,JWT也可以做到方便的維護(hù)過(guò)期時(shí)間,不做過(guò)多討論)如果想了解Security+JWT簡(jiǎn)易入門,請(qǐng)戳
本項(xiàng)目結(jié)構(gòu)如下

另外,本demo使用了MybatisPlus、lombok。
核心代碼
首先需要實(shí)現(xiàn)兩個(gè)類,一個(gè)是UserDetails的實(shí)現(xiàn)類SecurityUser,一個(gè)是UserDetailsService的實(shí)現(xiàn)類SecurityUserService。
**
* Security 要求需要實(shí)現(xiàn)的User類
* */
@Data
public class SecurityUser implements UserDetails {
@Autowired
private SysRoleService sysRoleService;
//用戶登錄名(注意此處的username和SysUser的loginName是一個(gè)值)
private String username;
//登錄密碼
private String password;
//用戶id
private SysUser sysUser;
//該用戶的所有權(quán)限
private List<SysMenu> sysMenuList;
/**構(gòu)造函數(shù)*/
public SecurityUser(SysUser sysUser){
this.username = sysUser.getLoginName();
this.password = sysUser.getPassword();
this.sysUser = sysUser;
}
public SecurityUser(SysUser sysUser,List<SysMenu> sysMenuList){
this.username = sysUser.getLoginName();
this.password = sysUser.getPassword();
this.sysMenuList = sysMenuList;
this.sysUser = sysUser;
}
/**需要實(shí)現(xiàn)的方法*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for(SysMenu menu : sysMenuList) {
authorities.add(new SimpleGrantedAuthority(menu.getPerms()));
}
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
//默認(rèn)賬戶未過(guò)期
@Override
public boolean isAccountNonExpired() {
return true;
}
//默認(rèn)賬戶沒(méi)有帶鎖
@Override
public boolean isAccountNonLocked() {
return true;
}
//默認(rèn)憑證沒(méi)有過(guò)期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//默認(rèn)賬戶可用
@Override
public boolean isEnabled() {
return true;
}
}
這個(gè)類包含著某個(gè)請(qǐng)求者的信息,在Security中叫做主體。其中這個(gè)方法是必須實(shí)現(xiàn)的,可以獲取用戶的具體權(quán)限。我們這邊權(quán)限的顆粒度達(dá)到了菜單級(jí)別,而不是很多開(kāi)源項(xiàng)目中角色那級(jí)別,我覺(jué)得顆粒度越細(xì)越方便(個(gè)人覺(jué)得...)
/**
* Security 要求需要實(shí)現(xiàn)的UserService類
* */
@Service
public class SecurityUserService implements UserDetailsService{
@Autowired
private SysUserService sysUserService;
@Autowired
private SysMenuService sysMenuService;
@Autowired
private HttpServletRequest httpServletRequest;
@Override
public SecurityUser loadUserByUsername(String loginName) throws UsernameNotFoundException {
LambdaQueryWrapper<SysUser> condition = Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, loginName);
SysUser sysUser = sysUserService.getOne(condition);
if (Objects.isNull(sysUser)){
throw new UsernameNotFoundException("未找到該用戶!");
}
Long projectId = null;
try{
projectId = Long.parseLong(httpServletRequest.getHeader("projectId"));
}catch (Exception e){
}
SysMenuModel sysMenuModel;
if (sysUser.getUserType()){
sysMenuModel = new SysMenuModel();
}else {
sysMenuModel = new SysMenuModel().setUserId(sysUser.getId());
}
sysMenuModel.setProjectId(projectId);
List<SysMenu> menuList = sysMenuService.getList(sysMenuModel);
return new SecurityUser(sysUser,menuList);
}
}
顯而易見(jiàn),這個(gè)類實(shí)現(xiàn)了唯一的方法loadUserByUsername,從而可以拿到某用戶的所有權(quán)限,并生成主體,在后面的filter中就可以見(jiàn)到他的作用了。
在看配置和filter之前,還有一個(gè)類需要說(shuō)明一下,此類提供方法,可以讓用戶未登錄、或者token失效的情況下進(jìn)行統(tǒng)一返回。
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = 1L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token失效,請(qǐng)登陸后重試");
}
}
ok,接下來(lái)看配置,實(shí)現(xiàn)了WebSecurityConfigurerAdapter的SecurityConfig類,特別說(shuō)明,本demo算是前后端分離的前提下寫的,所以實(shí)現(xiàn)過(guò)多的方法,其實(shí)這個(gè)類可以實(shí)現(xiàn)三個(gè)方法。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Autowired
SecurityFilter securityFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//禁止csrf
.csrf().disable()
//異常處理
.exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint).and()
//Session管理方式
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
//開(kāi)啟認(rèn)證
.authorizeRequests()
.antMatchers("/login/login").permitAll()
.antMatchers("/login/register").permitAll()
.antMatchers("/login/logout").permitAll()
.anyRequest().authenticated();
http
.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
}
}
異常處理就是上面那個(gè)類,Session那幾種管理方式我在那篇Security+JWT的文章中也有所講解,比較簡(jiǎn)單,然后是幾個(gè)不用驗(yàn)證的登錄路徑,剩下的都需要經(jīng)過(guò)我們下面這個(gè)filter。
@Slf4j
@Component
public class SecurityFilter extends OncePerRequestFilter {
@Autowired
SecurityUserService securityUserService;
@Autowired
SysUserService sysUserService;
@Autowired
SysUserTokenService sysUserTokenService;
/**
* 認(rèn)證授權(quán)
* */
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
log.info("訪問(wèn)的鏈接是:{}",httpServletRequest.getRequestURL());
try {
final String token = httpServletRequest.getHeader("token");
LambdaQueryWrapper<SysUserToken> condition = Wrappers.<SysUserToken>lambdaQuery().eq(SysUserToken::getToken, token);
SysUserToken sysUserToken = sysUserTokenService.getOne(condition);
if (Objects.nonNull(sysUserToken)){
SysUser sysUser = sysUserService.getById(sysUserToken.getUserId());
if (Objects.nonNull(sysUser)){
SecurityUser securityUser = securityUserService.loadUserByUsername(sysUser.getLoginName());
//將主體放入內(nèi)存
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//放入內(nèi)存中去
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}catch (Exception e){
log.error("認(rèn)證授權(quán)時(shí)出錯(cuò):{}", Arrays.toString(e.getStackTrace()));
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
判斷用戶是否登錄,就是從數(shù)據(jù)庫(kù)中查看是否有未過(guò)期的token,如果存在,就把主體信息放進(jìn)到項(xiàng)目的內(nèi)存中去,特別說(shuō)明的是,每個(gè)請(qǐng)求鏈結(jié)束,SecurityContextHolder.getContext()的數(shù)據(jù)都會(huì)被clear的,所以,每次請(qǐng)求的時(shí)候都需要set。
以上就完成了Security核心的創(chuàng)建,為了業(yè)務(wù)代碼方便獲取內(nèi)存中的主體信息,我特意加了一個(gè)獲取用戶信息的方法
/**
* 獲取Security主體工具類
* @author pjjlt
* */
public class SecurityUserUtil {
public static SysUser getCurrentUser(){
SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (Objects.nonNull(securityUser) && Objects.nonNull(securityUser.getSysUser())){
return securityUser.getSysUser();
}
return null;
}
}
業(yè)務(wù)代碼
以上是Security核心代碼,下面簡(jiǎn)單加兩個(gè)業(yè)務(wù)代碼,比如登錄和某個(gè)接口的權(quán)限訪問(wèn)測(cè)試。
萬(wàn)物之源登錄登出
首先,不被filter攔截的那三個(gè)方法注冊(cè)、登錄、登出,我都寫在了moudle.controller.LoginController這個(gè)路徑下,注冊(cè)就不用說(shuō)了,就是一個(gè)insertUser的方法,做好判斷就好,密碼通過(guò)AES加個(gè)密。
下面看下登錄代碼,controller層就不說(shuō)了,反正就是個(gè)驗(yàn)參。
/**
* 登錄,返回登錄信息,前端需要緩存
* */
@Override
@Transactional(rollbackFor = Exception.class)
public JSONObject login(SysUserModel sysUserModel) throws Exception{
JSONObject result = new JSONObject();
//1. 驗(yàn)證賬號(hào)是否存在、密碼是否正確、賬號(hào)是否停用
Wrapper<SysUser> sysUserWrapper = Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getLoginName,sysUserModel.getLoginName()).or()
.eq(SysUser::getEmail,sysUserModel.getEmail());
SysUser sysUser = baseMapper.selectOne(sysUserWrapper);
if (Objects.isNull(sysUser)){
throw new Exception("用戶不存在!");
}
String password = CipherUtil.encryptByAES(sysUserModel.getPassword());
if (!password.equals(sysUser.getPassword())){
throw new Exception("密碼不正確!");
}
if (sysUser.getStatus()){
throw new Exception("賬號(hào)已刪除或已停用!");
}
// 2.更新最后登錄時(shí)間
sysUser.setLoginIp(ServletUtil.getClientIP(request));
sysUser.setLoginDate(LocalDateTime.now());
baseMapper.updateById(sysUser);
// 3.封裝token,返回信息
String token = UUID.fastUUID().toString().replace("-","");
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeSeconds);
SysUserToken sysUserToken = new SysUserToken()
.setToken(token).setUserId(sysUser.getId()).setExpireTime(expireTime);
sysUserTokenService.save(sysUserToken);
result.putOpt("token",token);
result.putOpt("expireTime",expireTime);
return result;
}
首先驗(yàn)證下用戶是否存在,登錄密碼是否正確,然后封裝token,值得一提的是,我并沒(méi)有從數(shù)據(jù)庫(kù)(sysUserToken)中獲取用戶已經(jīng)登錄的token,然后更新過(guò)期時(shí)間的形式做登錄,而是每次登錄都獲取新token,這樣就可以做到多端登錄了,后期還可以做賬號(hào)登錄數(shù)量的控制。
然后就是登出,刪除庫(kù)中存在的token
/**
* 登出,刪除token
* */
@Override
public void logout() throws Exception{
String token = httpServletRequest.getHeader("token");
if (Objects.isNull(token)){
throw new LoginException("token不存在",ResultEnum.LOGOUT_ERROR);
}
LambdaQueryWrapper<SysUserToken> sysUserWrapper = Wrappers.<SysUserToken>lambdaQuery()
.eq(SysUserToken::getToken,token);
baseMapper.delete(sysUserWrapper);
}
權(quán)限驗(yàn)證
這邊我維護(hù)了兩個(gè)賬號(hào),一個(gè)是超級(jí)管理員majian,擁有所有權(quán)限。一個(gè)是普通人員_pjjlt,只有一些權(quán)限,我們看一下訪問(wèn)接口的效果。
我們?cè)L問(wèn)的接口是moudle.controller.LoginController路徑下的
@PreAuthorize("hasAnyAuthority('test')")
@GetMapping("test")
public String test(){
return "test";
}
其中hasAnyAuthority('test')就是權(quán)限碼
我們模擬用不同賬號(hào)訪問(wèn),就是改變請(qǐng)求header中的token值,就是登錄階段返回給前端的token。
首先是超級(jí)管理員驗(yàn)證

然后是普通管理員訪問(wèn)

接著沒(méi)有登錄(token不存在或者已過(guò)期)訪問(wèn)

demo地址
https://github.com/majian1994/easy-file-back
結(jié)束語(yǔ)
本文簡(jiǎn)單講解了,主要是將Security相關(guān)的東西,具體實(shí)現(xiàn)角色的三要素,用戶、角色、權(quán)限(菜單)可以看我的代碼,都寫完測(cè)完了,本來(lái)想寫個(gè)文檔管理系統(tǒng),幫助我司更好的管理接口文檔,but有位小伙伴找了一個(gè)不錯(cuò)的開(kāi)源的了,所以這代碼就成了我的一個(gè)小demo。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 設(shè)計(jì)模式在Spring框架中的應(yīng)用匯總
- Spring Boot接口設(shè)計(jì)防篡改、防重放攻擊詳解
- 淺談Spring中如何使用設(shè)計(jì)模式
- 詳解設(shè)計(jì)模式在Spring中的應(yīng)用(9種)
- Spring boot項(xiàng)目中異常攔截設(shè)計(jì)和處理詳解
- Spring之借助Redis設(shè)計(jì)一個(gè)簡(jiǎn)單訪問(wèn)計(jì)數(shù)器的示例
- 淺談spring中用到的設(shè)計(jì)模式及應(yīng)用場(chǎng)景
- 詳解spring整合shiro權(quán)限管理與數(shù)據(jù)庫(kù)設(shè)計(jì)
- 圖解Spring框架的設(shè)計(jì)理念與設(shè)計(jì)模式
- springboot多租戶設(shè)計(jì)過(guò)程圖解
相關(guān)文章
深入了解Java8中的時(shí)區(qū)日期時(shí)間
Java?在?java.time?包中也提供了幾個(gè)類用于處理需要關(guān)注時(shí)區(qū)的日期時(shí)間?API,本文將通過(guò)簡(jiǎn)單的示例講講它們的用法,需要的可以參考一下2023-04-04
java中ArrayList與LinkedList對(duì)比詳情
這篇文章主要通過(guò)實(shí)例對(duì)Java中ArrayList與LinkedList進(jìn)行了對(duì)比,需要的朋友可以參考下2017-04-04
Spring Security和Shiro的相同點(diǎn)與不同點(diǎn)整理
在本篇文章里小編給大家整理的是關(guān)于Spring Security和Shiro的相同不同點(diǎn)整理,需要的朋友們可以參考下。2020-02-02
在JDK和Eclipse下如何編寫和運(yùn)行Java Applet
本文主要介紹了在JDK和Eclipse的環(huán)境下如何編寫和運(yùn)行Java Applet,圖文方式,適合初學(xué)者學(xué)習(xí)。2015-09-09
如何對(duì)Mysql數(shù)據(jù)表查詢出來(lái)的結(jié)果進(jìn)行排序
這篇文章主要介紹了如何對(duì)Mysql數(shù)據(jù)表查詢出來(lái)的結(jié)果進(jìn)行排序問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
淺析Java ClassName.this中類名.this關(guān)鍵字的理解
Java ClassName.this中類名.this關(guān)鍵字 的理解大家都了解多少,有不太了解的朋友可以參考下本文一起學(xué)習(xí)學(xué)習(xí)2016-05-05
java并發(fā)之ArrayBlockingQueue詳細(xì)介紹
這篇文章主要介紹了java并發(fā)之ArrayBlockingQueue詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2017-05-05
Java實(shí)現(xiàn)多路復(fù)用select模型實(shí)例詳解
在計(jì)算機(jī)網(wǎng)絡(luò)中,多路復(fù)用(Multiplexing)指的是通過(guò)一種機(jī)制將多個(gè) I/O 操作合并到同一個(gè)線程或進(jìn)程中,從而提高系統(tǒng)的效率,在 Java 中,可以使用 Selector 類來(lái)實(shí)現(xiàn)基于 I/O 多路復(fù)用的模式,故本文給大家介紹了Java實(shí)現(xiàn)多路復(fù)用select模型實(shí)例,需要的朋友可以參考下2025-03-03

