SpringSecurity-前后端分離教程_第1頁
SpringSecurity-前后端分離教程_第2頁
SpringSecurity-前后端分離教程_第3頁
SpringSecurity-前后端分離教程_第4頁
SpringSecurity-前后端分離教程_第5頁
已閱讀5頁,還剩33頁未讀, 繼續(xù)免費閱讀

付費下載

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

SpringSecurity-前后端分離教程1、簡介SpringSecurity

是Spring家族中的一個安全管理框架。相比與另外一個安全框架Shiro,它提供了更豐富的功能,社區(qū)資源也比Shiro豐富。一般來說中大型的項目都是使用SpringSecurity

來做安全框架。小項目有Shiro的比較多,因為相比與SpringSecurity,Shiro的上手更加的簡單。一般Web應用的需要進行認證和授權。認證:驗證當前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認具體是哪個用戶授權:經過認證后判斷當前用戶是否有權限進行某個操作而認證和授權也是SpringSecurity作為安全框架的核心功能。2、快速入門1、準備工作我們先要搭建一個簡單的SpringBoot工程①創(chuàng)建SpringBoot工程、添加依賴<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>jectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>②創(chuàng)建Controller@RestControllerpublicclassHelloController{@RequestMapping("/hello")publicStringhello(){return"hello";}}③啟動項目引入依賴后我們在嘗試去訪問之前的接口就會自動跳轉到一個SpringSecurity的默認登陸頁面,默認用戶名是user,密碼會輸出在控制臺。必須登陸之后才能對接口進行訪問。3、認證1、登錄流程

2、認證原理2.1、SpringSecurity完整流程SpringSecurity的原理其實就是一個過濾器鏈,內部包含了提供各種功能的過濾器。這里我們可以看看入門案例中的過濾器。

圖中只展示了核心過濾器,其它的非核心過濾器并沒有在圖中展示。UsernamePasswordAuthenticationFilter:負責處理我們在登陸頁面填寫了用戶名密碼后的登陸請求。入門案例的認證工作主要有它負責。ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException。FilterSecurityInterceptor:負責權限校驗的過濾器。我們可以通過Debug查看當前系統(tǒng)中SpringSecurity過濾器鏈中有哪些過濾器及它們的順序。2.2、認證流程詳解概念速查:Authentication接口:它的實現類,表示當前訪問系統(tǒng)的用戶,封裝了用戶相關信息。AuthenticationManager接口:定義了認證Authentication的方法UserDetailsService接口:加載用戶特定數據的核心接口。里面定義了一個根據用戶名查詢用戶信息的方法。UserDetails接口:提供核心用戶信息。通過UserDetailsService根據用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。2.3、自定義實現認證流程分析2.3.1、思路分析登錄①自定義登錄接口1、調用ProviderManager的方法進行認證如果認證通過生成jwt2、把用戶信息存入redis中②自定義UserDetailsService1、在這個實現類中去查詢數據庫校驗:①定義Jwt認證過濾器1、獲取token2、解析token獲取其中的userid3、從redis中獲取用戶信息4、存入SecurityContextHolder2.3.2、準備工作①添加依賴<!--redis依賴--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--fastjson依賴--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.33</version></dependency><!--jwt依賴--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>②

添加Redis相關配置importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.serializer.SerializerFeature;importcom.fasterxml.jackson.databind.JavaType;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.type.TypeFactory;importorg.springframework.data.redis.serializer.RedisSerializer;importorg.springframework.data.redis.serializer.SerializationException;importcom.alibaba.fastjson.parser.ParserConfig;importorg.springframework.util.Assert;importjava.nio.charset.Charset;/***Redis使用FastJson序列化**@authorsg*/publicclassFastJsonRedisSerializer<T>implementsRedisSerializer<T>{publicstaticfinalCharsetDEFAULT_CHARSET=Charset.forName("UTF-8");privateClass<T>clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}publicFastJsonRedisSerializer(Class<T>clazz){super();this.clazz=clazz;}@Overridepublicbyte[]serialize(Tt)throwsSerializationException{if(t==null){returnnewbyte[0];}returnJSON.toJSONString(t,SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@OverridepublicTdeserialize(byte[]bytes)throwsSerializationException{if(bytes==null||bytes.length<=0){returnnull;}Stringstr=newString(bytes,DEFAULT_CHARSET);returnJSON.parseObject(str,clazz);}protectedJavaTypegetJavaType(Class<?>clazz){returnTypeFactory.defaultInstance().constructType(clazz);}}@ConfigurationpublicclassRedisConfig{@Bean@SuppressWarnings(value={"unchecked","rawtypes"})publicRedisTemplate<Object,Object>redisTemplate(RedisConnectionFactoryconnectionFactory){RedisTemplate<Object,Object>template=newRedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializerserializer=newFastJsonRedisSerializer(Object.class);//使用StringRedisSerializer來序列化和反序列化redis的key值template.setKeySerializer(newStringRedisSerializer());template.setValueSerializer(serializer);//Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(newStringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();returntemplate;}}③響應類

ViewCode④Jwt工具類

ViewCode⑤Redis工具類

ViewCode⑥將字符串響應到客戶端工具類importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;publicclassWebUtils{/***將字符串渲染到客戶端**@paramresponse渲染對象*@paramstring待渲染的字符串*@returnnull*/publicstaticStringrenderString(HttpServletResponseresponse,Stringstring){try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);}catch(IOExceptione){e.printStackTrace();}returnnull;}}⑦實體類(對應數據庫中的用戶表)

ViewCode2.3.3、具體實現1、數據庫校驗用戶從之前的分析我們可以知道,我們可以自定義一個UserDetailsService,讓SpringSecurity使用我們的UserDetailsService。我們自己的UserDetailsService可以從數據庫中查詢用戶名和密碼。

準備工作1、創(chuàng)建用戶表:CREATETABLE`sys_user`(`id`BIGINT(20)NOTNULLAUTO_INCREMENTCOMMENT'主鍵',`user_name`VARCHAR(64)NOTNULLDEFAULT'NULL'COMMENT'用戶名',`nick_name`VARCHAR(64)NOTNULLDEFAULT'NULL'COMMENT'昵稱',`password`VARCHAR(64)NOTNULLDEFAULT'NULL'COMMENT'密碼',`status`CHAR(1)DEFAULT'0'COMMENT'賬號狀態(tài)(0正常1停用)',`email`VARCHAR(64)DEFAULTNULLCOMMENT'郵箱',`phonenumber`VARCHAR(32)DEFAULTNULLCOMMENT'手機號',`sex`CHAR(1)DEFAULTNULLCOMMENT'用戶性別(0男,1女,2未知)',`avatar`VARCHAR(128)DEFAULTNULLCOMMENT'頭像',`user_type`CHAR(1)NOTNULLDEFAULT'1'COMMENT'用戶類型(0管理員,1普通用戶)',`create_by`BIGINT(20)DEFAULTNULLCOMMENT'創(chuàng)建人的用戶id',`create_time`DATETIMEDEFAULTNULLCOMMENT'創(chuàng)建時間',`update_by`BIGINT(20)DEFAULTNULLCOMMENT'更新人',`update_time`DATETIMEDEFAULTNULLCOMMENT'更新時間',`del_flag`INT(11)DEFAULT'0'COMMENT'刪除標志(0代表未刪除,1代表已刪除)',PRIMARYKEY(`id`))ENGINE=INNODBAUTO_INCREMENT=2DEFAULTCHARSET=utf8mb4COMMENT='用戶表'2、引入MybatisPlus和mysql驅動的依賴:<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>3、配置數據庫信息:spring:datasource:url:jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTCusername:rootpassword:rootdriver-class-name:com.mysql.cj.jdbc.Driver4、定義Mapper接口:publicinterfaceUserMapperextendsBaseMapper<User>{}5、修改User實體類:類名上加@TableName(value="sys_user"),id字段上加@TableId6、配置Mapper掃描:@SpringBootApplication@MapperScan("com.sangeng.mapper")publicclassSimpleSecurityApplication{publicstaticvoidmain(String[]args){ConfigurableApplicationContextrun=SpringApplication.run(SimpleSecurityApplication.class);System.out.println(run);}}核心代碼實現1、創(chuàng)建一個類實現UserDetailsService接口,重寫其中的方法。更加用戶名從數據庫中查詢用戶信息@ServicepublicclassUserDetailsServiceImplimplementsUserDetailsService{@AutowiredprivateUserMapperuserMapper;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{//根據用戶名查詢用戶信息LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);Useruser=userMapper.selectOne(wrapper);//如果查詢不到數據就通過拋出異常來給出提示if(Objects.isNull(user)){thrownewRuntimeException("用戶名或密碼錯誤");}//TODO根據用戶查詢權限信息添加到LoginUser中//封裝成UserDetails對象返回returnnewLoginUser(user);}}2、因為UserDetailsService方法的返回值是UserDetails類型,所以需要定義一個類,實現該接口,把用戶信息封裝在其中。

ViewCode注意:如果要測試,需要往用戶表中寫入用戶數據,并且如果你想讓用戶的密碼是明文存儲,需要在密碼前加{noop}。例如這樣登陸的時候就可以用sg作為用戶名,1234作為密碼來登陸了。2、密碼加密存儲實際項目中我們不會把密碼明文存儲在數據庫中。默認使用的PasswordEncoder要求數據庫中的密碼格式為:{id}password。它會根據id去判斷密碼的加密方式。但是我們一般不會采用這種方式。所以就需要替換PasswordEncoder。我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。我們只需要使用把BCryptPasswordEncoder對象注入Spring容器中,SpringSecurity就會使用該PasswordEncoder來進行密碼校驗。我們可以定義一個SpringSecurity的配置類,SpringSecurity要求這個配置類要繼承WebSecurityConfigurerAdapter。@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}}3、登錄接口接下我們需要自定義登陸接口,然后讓SpringSecurity對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。?在接口中我們通過AuthenticationManager的authenticate方法來進行用戶認證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。?認證成功的話要生成一個jwt,放入響應中返回。并且為了讓用戶下回請求時能通過jwt識別出具體的是哪個用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。@RestControllerpublicclassLoginController{@AutowiredprivateLoginServcieloginServcie;@PostMapping("/user/login")publicResponseResultlogin(@RequestBodyUseruser){returnloginServcie.login(user);}}@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http//關閉csrf.csrf().disable()//不通過Session獲取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//對于登錄接口允許匿名訪問.antMatchers("/user/login").anonymous()//除上面外的所有請求全部需要鑒權認證.anyRequest().authenticated();}@Bean@OverridepublicAuthenticationManagerauthenticationManagerBean()throwsException{returnsuper.authenticationManagerBean();}}@ServicepublicclassLoginServiceImplimplementsLoginServcie{@AutowiredprivateAuthenticationManagerauthenticationManager;@AutowiredprivateRedisCacheredisCache;@OverridepublicResponseResultlogin(Useruser){UsernamePasswordAuthenticationTokenauthenticationToken=newUsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());Authenticationauthenticate=authenticationManager.authenticate(authenticationToken);if(Objects.isNull(authenticate)){thrownewRuntimeException("用戶名或密碼錯誤");}//使用userid生成tokenLoginUserloginUser=(LoginUser)authenticate.getPrincipal();StringuserId=loginUser.getUser().getId().toString();Stringjwt=JwtUtil.createJWT(userId);//authenticate存入redisredisCache.setCacheObject("login:"+userId,loginUser);//把token響應給前端HashMap<String,String>map=newHashMap<>();map.put("token",jwt);returnnewResponseResult(200,"登陸成功",map);}}4、認證過濾器我們需要自定義一個過濾器,這個過濾器會去獲取請求頭中的token,對token進行解析取出其中的userid。使用userid去redis中獲取對應的LoginUser對象。然后封裝Authentication對象存入SecurityContextHolder@ComponentpublicclassJwtAuthenticationTokenFilterextendsOncePerRequestFilter{@AutowiredprivateRedisCacheredisCache;@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{//獲取tokenStringtoken=request.getHeader("token");if(!StringUtils.hasText(token)){//放行filterChain.doFilter(request,response);return;}//解析tokenStringuserid;try{Claimsclaims=JwtUtil.parseJWT(token);userid=claims.getSubject();}catch(Exceptione){e.printStackTrace();thrownewRuntimeException("token非法");}//從redis中獲取用戶信息StringredisKey="login:"+userid;LoginUserloginUser=redisCache.getCacheObject(redisKey);if(Objects.isNull(loginUser)){thrownewRuntimeException("用戶未登錄");}//存入SecurityContextHolder//TODO獲取權限信息封裝到Authentication中UsernamePasswordAuthenticationTokenauthenticationToken=newUsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request,response);}}@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@AutowiredJwtAuthenticationTokenFilterjwtAuthenticationTokenFilter;@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http//關閉csrf.csrf().disable()//不通過Session獲取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//對于登錄接口允許匿名訪問.antMatchers("/user/login").anonymous()//除上面外的所有請求全部需要鑒權認證.anyRequest().authenticated();//把token校驗過濾器添加到過濾器鏈中http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);}@Bean@OverridepublicAuthenticationManagerauthenticationManagerBean()throwsException{returnsuper.authenticationManagerBean();}}5、退出登陸我們只需要定義一個登陸接口,然后獲取SecurityContextHolder中的認證信息,刪除redis中對應的數據即可。@ServicepublicclassLoginServiceImplimplementsLoginServcie{@AutowiredprivateAuthenticationManagerauthenticationManager;@AutowiredprivateRedisCacheredisCache;@OverridepublicResponseResultlogin(Useruser){UsernamePasswordAuthenticationTokenauthenticationToken=newUsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());Authenticationauthenticate=authenticationManager.authenticate(authenticationToken);if(Objects.isNull(authenticate)){thrownewRuntimeException("用戶名或密碼錯誤");}//使用userid生成tokenLoginUserloginUser=(LoginUser)authenticate.getPrincipal();StringuserId=loginUser.getUser().getId().toString();Stringjwt=JwtUtil.createJWT(userId);//authenticate存入redisredisCache.setCacheObject("login:"+userId,loginUser);//把token響應給前端HashMap<String,String>map=newHashMap<>();map.put("token",jwt);returnnewResponseResult(200,"登陸成功",map);}@OverridepublicResponseResultlogout(){Authenticationauthentication=SecurityContextHolder.getContext().getAuthentication();LoginUserloginUser=(LoginUser)authentication.getPrincipal();Longuserid=loginUser.getUser().getId();redisCache.deleteObject("login:"+userid);returnnewResponseResult(200,"退出成功");}}三、授權1、權限系統(tǒng)的作用例如一個學校圖書館的管理系統(tǒng),如果是普通學生登錄就能看到借書還書相關的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個圖書館管理員的賬號登錄了,應該就能看到并使用添加書籍信息,刪除書籍信息等功能。總結起來就是不同的用戶可以使用不同的功能。這就是權限系統(tǒng)要去實現的效果。我們不能只依賴前端去判斷用戶的權限來選擇顯示哪些菜單哪些按鈕。因為如果只是這樣,如果有人知道了對應功能的接口地址就可以不通過前端,直接去發(fā)送請求來實現相關功能操作。所以我們還需要在后臺進行用戶權限的判斷,判斷當前用戶是否有相應的權限,必須具有所需權限才能進行相應的操作。2、授權基本流程在SpringSecurity中,會使用默認的FilterSecurityInterceptor來進行權限校驗。在FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權限信息。當前用戶是否擁有訪問當前資源所需的權限。所以我們在項目中只需要把當前登錄用戶的權限信息也存入Authentication。然后設置我們的資源所需要的權限即可。3、授權實現3.1、限制訪問資源所需權限SpringSecurity為我們提供了基于注解的權限控制方案,這也是我們項目中主要采用的方式。我們可以使用注解去指定訪問對應的資源所需的權限。但是要使用它我們需要先開啟相關配置。@EnableGlobalMethodSecurity(prePostEnabled=true)然后就可以使用對應的注解。@PreAuthorize@RestControllerpublicclassHelloController{@RequestMapping("/hello")@PreAuthorize("hasAuthority('test')")publicStringhello(){return"hello";}}3.2、封裝權限信息我們前面在寫UserDetailsServiceImpl的時候說過,在查詢出用戶后還要獲取對應的權限信息,封裝到UserDetails中返回。我們先直接把權限信息寫死封裝到UserDetails中進行測試。我們之前定義了UserDetails的實現類LoginUser,想要讓其能封裝權限信息就要對其進行修改。

ViewCodeLoginUser修改完后我們就可以在UserDetailsServiceImpl中去把權限信息封裝到LoginUser中了。我們寫死權限進行測試,后面我們再從數據庫中查詢權限信息。@ServicepublicclassUserDetailsServiceImplimplementsUserDetailsService{@AutowiredprivateUserMapperuserMapper;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);Useruser=userMapper.selectOne(wrapper);if(Objects.isNull(user)){thrownewRuntimeException("用戶名或密碼錯誤");}//TODO根據用戶查詢權限信息添加到LoginUser中List<String>list=newArrayList<>(Arrays.asList("test"));returnnewLoginUser(user,list);}}3.3、從數據庫查詢權限信息1、RBAC權限模型RBAC權限模型(Role-BasedAccessControl)即:基于角色的權限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權限模型。2、準備工作rbac需要創(chuàng)建表sql:

ViewCodemenu實體類創(chuàng)建:

ViewCode3、代碼實現我們只需要根據用戶id去查詢到其所對應的權限信息即可。所以我們可以先定義個mapper,其中提供一個方法可以根據userid查詢權限信息。publicinterfaceMenuMapperextendsBaseMapper<Menu>{List<String>selectPermsByUserId(Longid);}尤其是自定義方法,所以需要創(chuàng)建對應的mapper文件,定義對應的sql語句<?xmlversion="1.0"encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-////DTDMapper3.0//EN""/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.sangeng.mapper.MenuMapper"><selectid="selectPermsByUserId"resultType="java.lang.String">SELECTDISTINCTm.`perms`FROMsys_user_roleurLEFTJOIN`sys_role`rONur.`role_id`=r.`id`LEFTJOIN`sys_role_menu`rmONur.`role_id`=rm.`role_id`LEFTJOIN`sys_menu`mONm.`id`=rm.`menu_id`WHEREuser_id=#{userid}ANDr.`status`=0ANDm.`status`=0</select></mapper>在application.yml中配置mapperXML文件的位置spring:datasource:url:jdbc:mysql://localhost:3306/sg_security?characterEncoding=utf-8&serverTimezone=UTCusername:rootpassword:rootdriver-class-name:com.mysql.cj.jdbc.Driverredis:host:localhostport:6379mybatis-plus:mapper-locations:classpath*:/mapper/**/*.xml然后我們可以在UserDetailsServiceImpl中去調用該mapper的方法查詢權限信息封裝到LoginUser對象中即可。@ServicepublicclassUserDetailsServiceImplimplementsUserDetailsService{@AutowiredprivateUserMapperuserMapper;@AutowiredprivateMenuMappermenuMapper;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{LambdaQueryWrapper<User>wrapper=newLambdaQueryWrapper<>();wrapper.eq(User::getUserName,username);Useruser=userMapper.selectOne(wrapper);if(Objects.isNull(user)){thrownewRuntimeException("用戶名或密碼錯誤");}List<String>permissionKeyList=menuMapper.selectPermsByUserId(user.getId());////測試寫法//List<String>list=newArrayList<>(Arrays.asList("test"));returnnewLoginUser(user,permissionKeyList);}}四、自定義失敗處理我們還希望在認證失敗或者是授權失敗的情況下也能和我們的接口一樣返回相同結構的json,這樣可以讓前端能對響應進行統(tǒng)一的處理。要實現這個功能我們需要知道SpringSecurity的異常處理機制。在SpringSecurity中,如果我們在認證或者授權的過程中出現了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認證失敗還是授權失敗出現的異常。如果是認證過程中出現的異常會被封裝成AuthenticationException然后調用AuthenticationEntryPoint對象的方法去進行異常處理。如果是授權過程中出現的異常會被封裝成AccessDeniedException然后調用AccessDeniedHandler對象的方法去進行異常處理。所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給SpringSecurity即可。①自定義實現類授權失敗:@ComponentpublicclassAccessDeniedHandlerImplimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptionaccessDeniedException)throwsIOException,ServletException{ResponseResultresult=newResponseResult(HttpStatus.FORBIDDEN.value(),"權限不足");Stringjson=JSON.toJSONString(result);WebUtils.renderString(response,json);}}認證失?。篅ComponentpublicclassAuthenticationEntryPointImplimplementsAuthenticationEntryPoint{@Overridepublicvoidcommence(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionauthException)throwsIOException,ServletException{ResponseResultresult=newResponseResult(HttpStatus.UNAUTHORIZED.value(),"認證失敗請重新登錄");Stringjson=JSON.toJSONString(result);WebUtils.renderString(response,json);}}②配置給SpringSecurity先注入對應的處理器:@AutowiredprivateAuthenticationEntryPointauthenticationEntryPoint;@AutowiredprivateAccessDeniedHandleraccessDeniedHandler;然后我們可以使用HttpSecurity對象的方法去配置。http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);五、跨域瀏覽器出于安全的考慮,使用XMLHttpRequest對象發(fā)起HTTP請求時必須遵守同源策略,否則就是跨域的HTTP請求,默認情況下是被禁止的。同源策略要求源相同才能正常進行通信,即協議、域名、端口號都完全一致。前后端分離項目,前端項目和后端項目一般都不是同源的,所以肯定會存在跨域請求的問題。所以我們就要處理一下,讓前端能進行跨域請求。①先對SpringBoot配置,運行跨域請求@ConfigurationpublicclassCorsConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddCorsMappings(CorsRegistryregistry){//設置允許跨域的路徑registry.addMapping("/**")//設置允許跨域請求的域名.allowedOriginPatterns("*")//是否允許cookie.allowCredentials(true)//設置允許的請求方式.allowedMethods("GET","POST","DELETE","PUT")//設置允許的header屬性.allowedHeaders("*")//跨域允許時間.maxAge(3600);}}②開啟SpringSecurity的跨域訪問由于我們的資源都會收到SpringSecurity的保護,所以想要跨域訪問還要讓SpringSecurity運行跨域訪問。@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http//關閉csrf.csrf().disable()//不通過Session獲取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//對于登錄接口允許匿名訪問.antMatchers("/user/login").anonymous()//除上面外的所有請求全部需要鑒權認證.anyRequest().authenticated();//添加過濾器http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);//配置異常處理器http.exceptionHandling()//配置認證失敗處理器.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);//允許跨域http.cors();}六、遺留小問題1、其它權限校驗方法我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進行校驗。SpringSecurity還為我們提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。這里我們先不急著去介紹這些方法,我們先去理解hasAuthority的原理,然后再去學習其他方法你就更容易理解,而不是死記硬背區(qū)別。并且我們也可以選擇定義校驗方法,實現我們自己的校驗邏輯。hasAuthority方法實際是執(zhí)行到了SecurityExpressionRoot的hasAuthority,大家只要斷點調試既可知道它內部的校驗原理。它內部其實是調用authentication的getAuthorities方法獲取用戶的權限列表。然后判斷我們存入的方法參數數據在權限列表中。hasAnyAuthority方法可以傳入多個權限,只有用戶有其中任意一個權限都可以訪問對應資源。@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")publicStringhello(){return"hello";}hasRole要求有對應的角色才可以訪問,但是它內部會把我們傳入的參數拼接上

ROLE_

后再去比較。所以這種情況下要用用戶對應的權限也要有

ROLE_

這個前綴才可以。@PreAuthorize("hasRole('system:dept:list')")publicStringhello(){return"hello";}hasAnyRole有任意的角色就可以訪問。它內部也會把我們傳入的參數拼接上

ROLE_

后再去比較。所以這種情況下要用用戶對應的權限也要有

ROLE_

這個前綴才可以。@PreAuthorize("hasAnyRole('admin','system:dept:list')")publicStringhello(){return"hello";}2、自定義權限校驗方法也可以定義自己的權限校驗方法,在@PreAuthorize注解中使用我們的方法。@Component("ex")publicclassSGExpressionRoot{publicbooleanhasAuthority(Stringauthority){//獲取當前用戶的權限Authenticationauthentication=SecurityContextHolder.getContext().getAuthentication();LoginUserloginUser=(LoginUser)authentication.getPrincipal();List<String>permissions=loginUser.getPermissions();//判斷用戶權限集合中是否存在authorityreturnpermissions.contains(authority);}}在SPEL表達式中使用@ex相當于獲取容器中bean的名字未ex的對象。然后再調用這個對象的hasAuthority方法@RequestMapping("/hello")@PreAuthorize("@ex.hasAuthority('system:dept:list')")publicStringhello(){return"hello";}3、基于配置的權限控制我們也可以在配置類中使用使用配置的方式對資源進行權限控制。@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http//關閉csrf.csrf().disable()//不通過Session獲取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//對于登錄接口允許匿名訪問.antMatchers("/user/login").anonymous().antMatchers("/testCors").hasAuthority("system:dept:list222")//除上面外的所有請求全部需要鑒權認證.anyRequest().authenticated();//添加過濾器http.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);//配置異常處理器http.exceptionHandling()//配置認證失敗處理器.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);//允許跨域http.cors();}4、CSRFCSRF是指跨站請求偽造(Cross-siterequestforgery),是web常見的攻擊之一。/freeking101/article/details/86537087SpringSecurity去防止CSRF攻擊的方式就是通過csrf_token。后端會生成一個csrf_token,前端發(fā)起請求的時候需要攜帶這個csrf_token,后端會有過濾器進行校驗,如果沒有攜帶或者是偽造的就不允許訪問。我們可以發(fā)現CSRF攻擊依靠的是cookie中所攜帶的認證信息。但是在前后端分離的項目中我們的認證信息其實是token,而token并不是存儲中cookie中,并且需要前端代碼去把token設置到請求頭中才可以,所以CSRF攻擊也就不用擔心了。5、認證成功處理器實際上在UsernamePasswordAuthenticationFilte

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論