為什么選擇SpringSecurity?
創(chuàng)新互聯(lián)自成立以來(lái),一直致力于為企業(yè)提供從網(wǎng)站策劃、網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作、網(wǎng)站建設(shè)、外貿(mào)網(wǎng)站建設(shè)、電子商務(wù)、網(wǎng)站推廣、網(wǎng)站優(yōu)化到為企業(yè)提供個(gè)性化軟件開發(fā)等基于互聯(lián)網(wǎng)的全面整合營(yíng)銷服務(wù)。公司擁有豐富的網(wǎng)站建設(shè)和互聯(lián)網(wǎng)應(yīng)用系統(tǒng)開發(fā)管理經(jīng)驗(yàn)、成熟的應(yīng)用系統(tǒng)解決方案、優(yōu)秀的網(wǎng)站開發(fā)工程師團(tuán)隊(duì)及專業(yè)的網(wǎng)站設(shè)計(jì)師團(tuán)隊(duì)。
現(xiàn)如今,在JavaWeb的世界里Spring可以說(shuō)是一統(tǒng)江湖,隨著微服務(wù)的到來(lái),SpringCloud可以說(shuō)是Java程序員必須熟悉的框架,就連阿里都為SpringCloud寫開源呢。(比如大名鼎鼎的Nacos)作為Spring的親兒子,SpringSecurity很好的適應(yīng)了了微服務(wù)的生態(tài)。你可以非常簡(jiǎn)便的結(jié)合Oauth做認(rèn)證中心服務(wù)。本文先從最簡(jiǎn)單的單體項(xiàng)目開始,逐步掌握Security。更多可達(dá)官方文檔
準(zhǔn)備
我準(zhǔn)備了一個(gè)簡(jiǎn)單的demo,具體代碼會(huì)放到文末。提前聲明,本demo沒有用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)賬戶沒有帶鎖 @Override public boolean isAccountNonLocked() { return true; } //默認(rèn)憑證沒有過(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í)別,而不是很多開源項(xiàng)目中角色那級(jí)別,我覺得顆粒度越細(xì)越方便(個(gè)人覺得...)
/** * 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); } }
顯而易見,這個(gè)類實(shí)現(xiàn)了唯一的方法loadUserByUsername,從而可以拿到某用戶的所有權(quán)限,并生成主體,在后面的filter中就可以見到他的作用了。
在看配置和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() //開啟認(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,值得一提的是,我并沒有從數(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)
接著沒有登錄(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ò)的開源的了,所以這代碼就成了我的一個(gè)小demo。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。
當(dāng)前文章:SpringSecurity單項(xiàng)目權(quán)限設(shè)計(jì)過(guò)程解析
URL鏈接:http://www.rwnh.cn/article4/ghcsoe.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站收錄、定制網(wǎng)站、品牌網(wǎng)站建設(shè)、動(dòng)態(tài)網(wǎng)站、網(wǎng)站維護(hù)、App設(shè)計(jì)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)