1、前言部分
1.1、如何學習?
看springsecurtiy原理圖的時候以為灑灑水,結果自己動手做的時候一竅不通,所以一定不要眼高手低,實踐出真知!通過各種方式學習springsecurity,在B站、騰訊課堂、網易課堂、慕課網沒有springsecurity的前後端分離的教學視頻,那我就去csdn去尋找springsecurity博客,發現幾個問題:
要麼就是前後端不分離,要麼就是通過內存方式讀取數據,而不是通過資料庫的方式讀取數據,要麼就是大佬們給的代碼不全、把代碼講的太繞,關鍵部分沒有注釋。講的例子不那麼通俗易懂,不利於新手的學習。代碼本身有bug,或者就沒有我想要實現的效果。 實在不行我又跑去github上找開源項目學習,github由於是外國網站,國內訪問速度有點慢!!那就用國內的gitee吧,gitee上的開源項目都是結合實戰項目的,代碼邏輯也比較複雜,我對項目的業務邏輯沒什麼了解,感覺不適合我。我這一次選擇比較反人性的方式去學習,就是手撕源碼和看官方文檔。老實講,剛開始看源碼和官方文檔特別難受,並且看不進去,那些springsecurity的類還有接口名字又臭又長,這時我就下載源碼,源碼的注釋多的就像一本書,非常詳細且權威。
當然別指望看幾遍就能看懂,我看這些注釋、源碼、博客看了10幾遍甚至20幾遍才看懂,每次去看都有不同的收穫!!!
此文章截圖水平不高、理解為主、欣賞為輔!!內容有點多,每一步都有詳細解析,請耐心看完,看不懂可以多看幾遍。。
1.2、技術支持
jdk 1.8、springboot 2.3.4、mybatis-plus 3.4.1、mysql 5.5、springsecurity 5.3.4、springmvc、lombok簡化entity代碼,不用你去寫get、set方法,全部自動生成、gson 2.8.2 將json對象轉化成json字符串
1.3、預期實現效果圖
未登錄時訪問指定資源, 返回未登錄的json字符串 , index是我在controller層寫的一個簡單接口,返回index字符串
輸入帳號錯誤,返回用戶名錯誤的json字符串 , 需說明一點,/login是springsecurity封裝好的接口,無須你在controller寫login接口,/logout也同理。
輸入密碼錯誤,返回密碼錯誤的json字符串
登錄成功, 返回登錄成功的json字符串並返回cookie
登錄成功並且擁有權限訪問指定資源, 返回資源相關數據的json字符串
登錄成功但無權限訪問指定資源時,返回權限不足的json字符串
異地登錄,返回異地登錄,強制下線的json字符串 , 測試的基礎是要在兩臺不同的機器上登錄,然後訪問/index。
註銷成功,返回註銷成功的json字符串並刪除cookie
2、核心部分
2.1、springsecurity原理解釋:
springsecurity最重要的兩個部分: authentication(認證) 和 authorization(授權)
認證: 就是判定你是什麼身份,管理員還是普通人
授權: 什麼樣的身份擁有什麼樣的權利。
簡單理解: 自定義配置登錄成功、登陸失敗、註銷成功目標結果類,並將其注入到springsecurity的配置文件中。如何認證、授權交給AuthenticationManager去作
複雜理解:
(1)用戶發起表單登錄請求後,首先進入
UsernamePasswordAuthenticationFilter
, 在 UsernamePasswordAuthenticationFilter中根據用戶輸入的用戶名、密碼構建了
UsernamePasswordAuthenticationToken
,並將其交給 AuthenticationManager 來進行認證處理。
AuthenticationManager 本身不包含認證邏輯,其核心是用來管理所有的
AuthenticationProvider
,通過交由合適的 AuthenticationProvider 來實現認證。
(2)下面跳轉到了
SelfAuthenticationProvider
,該類是 AuthenticationProvider 的實現類:你可以在該類的
Authentication authenticate(Authentication authentication)
自定義認證邏輯, 然後在該類中通過調用
UserDetails loadUserByUsername(account)
去獲取資料庫用戶信息並驗證,然後創建
並將權限、用戶個人信息注入到其中 ,並通過
setAuthenticated(true)
設置為需要驗證。
(3) 至此認證信息就被傳遞迴 UsernamePasswordAuthenticationFilter 中,在 UsernamePasswordAuthenticationFilter 的父類
AbstractAuthenticationProcessingFilter
的
doFilter()
中,會根據認證的成功或者失敗調用相應的 handler:所謂的handler就是我們注入到springsecurity配置文件的handler。
2.2、踩坑集錦
訪問/login時必須要用post方法!, 訪問的參數名必須為username和password 訪問/logout時即可用post也可用get方法!//springsecurity配置文件中的hasRole("")不能以ROLE開頭,比如ROLE_USER就是錯的,springsecurity會默認幫我們加上,但資料庫的權限欄位必須是ROLE_開頭,否則讀取不到.antMatchers("/index").hasRole("USER") .antMatchers("/hello").hasRole("ADMIN")
2.3、代碼部分
pom依賴文件
<dependencies> <!--轉換成json字符串的工具--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.2</version> </dependency> <!--springboot集成web操作7--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--springsecurity--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--mysql驅動--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--lombok依賴--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--mybatis-plus依賴--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <!--springboot-自帶測試工具--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.3.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.3.4.RELEASE</version> </dependency> </dependencies>
Msg.java 自定義返回結果集,這個看個人的,怎麼開心怎麼來!
@Data@NoArgsConstructor@AllArgsConstructorpublic class Msg {int code; //錯誤碼 String Message; //消息提示 Map<String,Object> data=new HashMap<String,Object>(); //數據 //無權訪問 public static Msg denyAccess(String message){ Msg result=new Msg(); result.setCode(300); result.setMessage(message); return result; } //操作成功 public static Msg success(String message){ Msg result=new Msg(); result.setCode(200); result.setMessage(message); return result; } //客戶端操作失敗 public static Msg fail(String message){ Msg result=new Msg(); result.setCode(400); result.setMessage(message); return result; } public Msg add(String key,Object value){ this.data.put(key,value); return this; }}
User.java ,此類是entity實體類
@Datapublic class User implements Serializable {private Integer id; private String account; private String password; private String role;}
UserMapper.java ,此接口繼承 BaseMapper<T>類,而BaseMapper<T>類 封裝了大量的sql,極大程度簡化了程式設計師sql語句的書寫.
@Repositorypublic interface UserMapper extends BaseMapper<User> {}
正常情況下要寫UserService.java接口,但是此文章只是用於演示效果,就沒書寫了
public interface UserService{}
UserServiceImpl.java,使其實現 UserDetailsService接口, 從而去獲取資料庫用戶信息,詳細解析請看注釋部分。
@Servicepublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService,UserDetailsService {@Autowired UserMapper userMapper; //加載用戶 @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { //mybatis-plus幫我們寫好了sql語句,相當於 select * from user where account ='${account}' QueryWrapper<User> wrapper=new QueryWrapper<>(); wrapper.eq("account",s); User user=userMapper.selectOne(wrapper); //user即為查詢結果 if(user==null){ throw new UsernameNotFoundException("用戶名錯誤!!"); } //獲取用戶權限,並把其添加到GrantedAuthority中 List<GrantedAuthority> grantedAuthorities=new ArrayList<>(); GrantedAuthority grantedAuthority=new SimpleGrantedAuthority(user.getRole()); grantedAuthorities.add(grantedAuthority); //方法的返回值要求返回UserDetails這個數據類型, UserDetails是接口,找它的實現類就好了 //new org.springframework.security.core.userdetails.User(String username,String password,Collection<? extends GrantedAuthority> authorities) 就是它的實現類 return new org.springframework.security.core.userdetails.User(s,user.getPassword(),grantedAuthorities); }}
UserController.java 就是普通的controller
@RestControllerpublic class UserController {@GetMapping("index") public String index(){ return "index"; } @GetMapping("hello") public String hello(){ return "hello"; }}
AuthenticationEnryPoint .java 自定義未登錄的處理邏輯
@Componentpublic class AuthenticationEnryPoint implements AuthenticationEntryPoint {@Autowired Gson gson; //未登錄時返回給前端數據 @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { Msg result=Msg.fail("需要登錄!!"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); }}
AuthenticationFailure.java 自定義登錄失敗時的處理邏輯
//登錄失敗返回給前端消息@Componentpublic class AuthenticationFailure implements AuthenticationFailureHandler{@Autowired Gson gson; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { Msg msg=null; if(e instanceof UsernameNotFoundException){ msg=Msg.fail(e.getMessage()); }else if(e instanceof BadCredentialsException){ msg=Msg.fail("密碼錯誤!!"); }else { msg=Msg.fail(e.getMessage()); } //處理編碼方式,防止中文亂碼的情況 response.setContentType("text/json;charset=utf-8"); //返回給前臺 response.getWriter().write(gson.toJson(msg)); }}
AuthenticationSuccess.java 自定義登錄成功時的處理邏輯
@Componentpublic class AuthenticationSuccess implements AuthenticationSuccessHandler{@Autowired Gson gson; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //登錄成功時返回給前端的數據 Msg result=Msg.success("登錄成功!!!!!"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); }}
AuthenticationLogout.java 自定義註銷時的處理邏輯
@Componentpublic class AuthenticationLogout implements LogoutSuccessHandler{@Autowired Gson gson; @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Msg result=Msg.success("註銷成功"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); }}
AccessDeny.java 自定義無權限訪問時的邏輯處理
//無權訪問@Componentpublic class AccessDeny implements AccessDeniedHandler{@Autowired Gson gson; @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { Msg result= Msg.denyAccess("無權訪問,need Authorities!!"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); }}
SessionInformationExpiredStrategy.java 自定義異地登錄、帳號下線時的邏輯處理
@Componentpublic class SessionInformationExpiredStrategy implements org.springframework.security.web.session.SessionInformationExpiredStrategy{@Autowired Gson gson; @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { Msg result= Msg.fail("您的帳號在異地登錄,建議修改密碼"); HttpServletResponse response=event.getResponse(); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); }}
SelfAuthenticationProvider.java 自定義認證邏輯處理
@Componentpublic class SelfAuthenticationProvider implements AuthenticationProvider{@Autowired UserServiceImpl userServiceImpl; @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String account= authentication.getName(); //獲取用戶名 String password= (String) authentication.getCredentials(); //獲取密碼 UserDetails userDetails= userServiceImpl.loadUserByUsername(account); boolean checkPassword= bCryptPasswordEncoder.matches(password,userDetails.getPassword()); if(!checkPassword){ throw new BadCredentialsException("密碼不正確,請重新登錄!"); } return new UsernamePasswordAuthenticationToken(account,password,userDetails.getAuthorities()); } @Override public boolean supports(Class<?> aClass) { return true; }}
SpringsecurityConfig.java是springsecurity的配置,詳細解析請看注釋!!
@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true) //開啟權限註解,默認是關閉的public class SpringsecurityConfig extends WebSecurityConfigurerAdapter {@Autowired AuthenticationEnryPoint authenticationEnryPoint; //未登錄 @Autowired AuthenticationSuccess authenticationSuccess; //登錄成功 @Autowired AuthenticationFailure authenticationFailure; //登錄失敗 @Autowired AuthenticationLogout authenticationLogout; //註銷 @Autowired AccessDeny accessDeny; //無權訪問 @Autowired SessionInformationExpiredStrategy sessionInformationExpiredStrategy; //檢測異地登錄 @Autowired SelfAuthenticationProvider selfAuthenticationProvider; //自定義認證邏輯處理 @Bean public UserDetailsService userDetailsService() { return new UserServiceImpl(); } //加密方式 @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } //認證 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(selfAuthenticationProvider); } //授權 @Override protected void configure(HttpSecurity http) throws Exception { //cors()解決跨域問題,csrf()會與restful風格衝突,默認springsecurity是開啟的,所以要disable()關閉一下 http.cors().and().csrf().disable(); // /index需要權限為ROLE_USER才能訪問 /hello需要權限為ROLE_ADMIN才能訪問 http.authorizeRequests() .antMatchers("/index").hasRole("USER") .antMatchers("/hello").hasRole("ADMIN") .and() .formLogin() //開啟登錄 .permitAll() //允許所有人訪問 .successHandler(authenticationSuccess) // 登錄成功邏輯處理 .failureHandler(authenticationFailure) // 登錄失敗邏輯處理 .and() .logout() //開啟註銷 .permitAll() //允許所有人訪問 .logoutSuccessHandler(authenticationLogout) //註銷邏輯處理 .deleteCookies("JSESSIONID") //刪除cookie .and().exceptionHandling() .accessDeniedHandler(accessDeny) //權限不足的時候的邏輯處理 .authenticationEntryPoint(authenticationEnryPoint) //未登錄是的邏輯處理 .and() .sessionManagement() .maximumSessions(1) //最多只能一個用戶登錄一個帳號 .expiredSessionStrategy(sessionInformationExpiredStrategy) //異地登錄的邏輯處理 ; }}
application.yml配置文件
server:port: 80spring: datasource: url: jdbc:mysql://localhost:3306/springsecurity_test?characterEncoding=utf8&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver