【轉】springboot整合spring security(一)
原文作者:王文健
來源:CSDN
轉自原文:https://blog.csdn.net/qq_29580525/article/details/79317969
但是原文有幾處錯誤,且本文也結合了其他自己的知識,可以說是上文的升級版本
一、Spring security 是什麼?
Spring Security是一個能夠為基於Spring的企業應用系統提供宣告式的安全訪問控制解決方案的安全框架。
它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面程式設計)功能,為應用系統提供宣告式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複程式碼的工作。
二、Spring security 怎麼使用?
使用Spring Security很簡單,只要在pom.xml檔案中,引入spring security的依賴就可以了。
<!-- spring security依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
什麼都不做,直接執行程式,這時你訪問任何一個URL,都會彈出一個“需要授權”的驗證框,如圖:
,spring security 會預設使用一個使用者名稱為:user 的使用者,密碼就是 啟動的時候生成的(通過控制檯console中檢視),如圖
然後在使用者名稱中輸入:user 密碼框中輸入 上面的密碼 ,之後就可以正常訪問之前URL了。很顯然這根本不是我們想要的,接下來我們需要一步一步的改造。
改造1 使用頁面表單登入
通過修改Security的配置來實現 參考:https://docs.spring.io/spring-security/site/docs/current/guides/html5//helloworld-boot.html#creating-your-spring-security-configuration
首先 新增一個類 SecurityConfig 繼承 WebSecurityConfigurerAdapter ,
重寫configure方法。
並加上@Configuration 和@EnableWebSecurity 2個註解。
//這是安全控制中心
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
//super.configure(http);
http
.formLogin().loginPage("/login").loginProcessingUrl("/login/form").failureUrl("/login-error").permitAll() //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
}
}
View Code
loginPage("/login")表示登入時跳轉的頁面,因為登入頁面我們不需要登入認證,所以我們需要新增 permitAll() 方法。
新增一個控制器,對應/login 返回一個登入頁面。
@RequestMapping("/login")
public String userLogin(){
return "demo-sign";
}
demo_sign.html 的 html部分程式碼如下:
<form class="form-signin" action="/login/form" method="post">
<h2 class="form-signin-heading">使用者登入</h2>
<table>
<tr>
<td>使用者名稱:</td>
<td><input type="text" name="username" class="form-control" placeholder="請輸入使用者名稱"/></td>
</tr>
<tr>
<td>密碼:</td>
<td><input type="password" name="password" class="form-control" placeholder="請輸入密碼" /></td>
</tr>
<tr>
<td colspan="2">
<button type="submit" class="btn btn-lg btn-primary btn-block" >登入</button>
</td>
</tr>
</table>
</form>
需要注意下:form提交的url要和配置檔案中的 loginProcessingUrl("")中的一致。
failureUrl=表示登入出錯的頁面,我們可以簡單寫個提示:如 使用者名稱或密碼錯誤。
@RequestMapping("/login-error")
public String loginError(){
return "login-error";
}
login-error.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>使用者登入</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/sign.css" />
</head>
<body>
<h3>使用者名稱或密碼錯誤</h3>
</body>
</html>
執行程式:如果輸入錯誤的使用者名稱和密碼的話,則會顯示如下圖所示:
我們用一個測試的RestController來測試
@RestController
public class HelloWorldController {
@RequestMapping("/hello")
public String helloWorld(){
return "spring security hello world";
}
}
當沒有登入時,輸入 http://localhost:port/hello 時,則直接跳轉到我們登入頁面,登入成功之後,再訪問 時,就能顯示我們期望的值了。
改造2、自定義使用者名稱和密碼
很顯然,這樣改造之後,雖然登入頁面是好看了,但還遠遠不能滿足我們的應用需求,所以第二步,我們改造自定義的使用者名稱和密碼。
自定義使用者名稱和密碼有2種方式,一種是在程式碼中寫死,這也是官方的demo,另一種是使用資料庫
首先是第一種:如
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
我們也照樣,這是把使用者名稱改成 admin 密碼改成 123456 roles是該使用者的角色,我們後面再細說。
還有種方法 就是 重寫 另外一種configure(AuthenticationManagerBuilder auth) 方法,這個和上面那個方法的作用是一樣的。選其一就可。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth
.inMemoryAuthentication()
.withUser("admin").password("123456").roles("USER")
.and()
.withUser("test").password("test123").roles("ADMIN");
}
注意,到這一步,舊版本的springsecurity就已經可以運行了,但是新版本下,必須要做進一步操作,
這是因為Spring boot 2.0.3引用的security 依賴是 spring security 5.X版本,此版本需要提供一個PasswordEncorder的例項,否則後臺彙報錯誤: java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 並且頁面毫無響應。 因此,需要建立PasswordEncorder的實現類。
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
在安全控制中心SecurityConfig下配置改為:
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
////不加.passwordEncoder(new MyPasswordEncoder())
// //就不是以明文的方式進行匹配,會報錯
//加的話在頁面提交時候,密碼會以明文的方式進行匹配
.passwordEncoder(new MyPasswordEncoder())
//這是把使用者名稱改成 admin 密碼改成 123456 roles是該使用者的角色,我們後面再細說。
.withUser("user").password("password").roles("USER")
.and()
.withUser("user1").password("password").roles("USER");//可以多寫幾個User
}
程式執行起來,這時用我們自己的使用者名稱和密碼 輸入 admin 和123456 就可以了
你也可以多幾個使用者,就多幾個withUser即可。
.and().withUser("test").password("test123").roles("ADMIN"); 這樣我們就有了一個使用者名稱為test,密碼為test123的使用者了。
第一種的只是讓我們體驗了一下Spring Security而已,我們接下來就要提供自定義的使用者認證機制及處理過程。
在講這個之前,我們需要知道spring security的原理,spring security的原理就是使用很多的攔截器對URL進行攔截,以此來管理登入驗證和使用者許可權驗證。
使用者登陸,會被AuthenticationProcessingFilter攔截,呼叫AuthenticationManager的實現,而且AuthenticationManager會呼叫ProviderManager來獲取使用者驗證資訊(不同的Provider呼叫的服務不同,因為這些資訊可以是在資料庫上,可以是在LDAP伺服器上,可以是xml配置檔案上等),如果驗證通過後會將使用者的許可權資訊封裝一個User放到spring的全域性快取SecurityContextHolder中,以備後面訪問資源時使用。
所以我們要自定義使用者的校驗機制的話,我們只要實現自己的AuthenticationProvider就可以了。在用AuthenticationProvider 這個之前,我們需要提供一個獲取使用者資訊的服務,實現 UserDetailsService 介面
使用者名稱密碼->(Authentication(未認證) -> AuthenticationManager ->AuthenticationProvider->UserDetailService->UserDetails->Authentication(已認證)
瞭解了這個原理之後,我們就開始寫程式碼
第一步:我們定義自己的使用者資訊類 UserInfo 繼承UserDetails和Serializable介面
程式碼如下:
class UserInfo implements Serializable, UserDetails {
/**
*
*/
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String role;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
public UserInfo(String username, String password, String role, boolean accountNonExpired, boolean accountNonLocked,
boolean credentialsNonExpired, boolean enabled) {
// TODO Auto-generated constructor stub
this.username = username;
this.password = password;
this.role = role;
this.accountNonExpired = accountNonExpired;
this.accountNonLocked = accountNonLocked;
this.credentialsNonExpired = credentialsNonExpired;
this.enabled = enabled;
}
// 這是許可權
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
}
@Override
public String getPassword() {
// TODO Auto-generated method stub
return password;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return username;
}
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return enabled;
}
}
View Code
然後實現第2個類 UserService 來返回這個UserInfo的物件例項
@Component
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
//這裡可以可以通過username(登入時輸入的使用者名稱)然後到資料庫中找到對應的使用者資訊,並構建成我們自己的UserInfo來返回。
return ;
}
}
// TODO Auto-generated method stub
//這裡可以通過資料庫來查詢到實際的使用者資訊,這裡我們先模擬下,後續我們用資料庫來實現
if(username.equals("admin"))
{
//假設返回的使用者資訊如下;
UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);
return userInfo;
}
return ;
}
到這裡為止,我們自己定義的UserInfo類和從資料庫中返回具體的使用者資訊已經實現,接下來我們要實現的,我們自己的 AuthenticationProvider
新建類 MyAuthenticationProvider 繼承AuthenticationProvider
完整的程式碼如下:
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
/**
* 注入我們自己定義的使用者資訊獲取物件
*/
@Autowired
private UserDetailsService userDetailService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// TODO Auto-generated method stub
String userName = authentication.getName();// 這個獲取表單輸入中返回的使用者名稱;
String password = (String) authentication.getCredentials();// 這個是表單中輸入的密碼;
// 這裡構建來判斷使用者是否存在和密碼是否正確
UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 這裡呼叫我們的自己寫的獲取使用者的方法;
if (userInfo == ) {
throw new BadCredentialsException("使用者名稱不存在");
}
// //這裡我們還要判斷密碼是否正確,實際應用中,我們的密碼一般都會加密,以Md5加密為例
// Md5PasswordEncoder md5PasswordEncoder=new Md5PasswordEncoder();
// //這裡第個引數,是salt
// 就是加點鹽的意思,這樣的好處就是使用者的密碼如果都是123456,由於鹽的不同,密碼也是不一樣的,就不用怕相同密碼洩漏之後,不會批量被破解。
// String encodePwd=md5PasswordEncoder.encodePassword(password, userName);
// //這裡判斷密碼正確與否
// if(!userInfo.getPassword().equals(encodePwd))
// {
// throw new BadCredentialsException("密碼不正確");
// }
// //這裡還可以加一些其他資訊的判斷,比如使用者賬號已停用等判斷,這裡為了方便我接下去的判斷,我就不用加密了。
//
//
if (!userInfo.getPassword().equals("123456")) {
throw new BadCredentialsException("密碼不正確");
}
Collection<? extends GrantedAuthority> authorities = userInfo.getAuthorities();
// 構建返回的使用者登入成功的token
return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
// 這裡直接改成retrun true;表示是支援這個執行
return true;
}
}
到此為止,我們的使用者資訊的獲取,校驗部分已經完成了。接下來要讓它起作用,則我們需要在配置檔案中修改,讓他起作用。回到我的SecurityConfig程式碼檔案,修改如下:
1、注入我們自己的AuthenticationProvider
2、修改配置的方法:
@Autowired
private MyAuthenticationProvider myAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth.authenticationProvider(provider);
// auth
// .inMemoryAuthentication()
// .withUser("admin").password("123456").roles("USER")
// .and()
// .withUser("test").password("test123").roles("ADMIN");
}
現在重新執行程式,則需要輸入使用者名稱為 admin 密碼是123456之後,才能正常登入了。
改造3、自定義登入成功和失敗的處理邏輯
在現在的大多數應用中,一般都是前後端分離的,所以我們登入成功或失敗都需要用json格式返回,或者登入成功之後,跳轉到某個具體的頁面。
接下來我們來實現這種改造。
為了實現這個功能,我們需要寫2個類,分別繼承SavedRequestAwareAuthenticationSuccessHandler和SimpleUrlAuthenticationFailureHandler2個類,並重寫其中的部分方法即可。
@Component("myAuthenticationSuccessHandler")
////處理登入成功的。
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
//什麼都不做的話,那就直接呼叫父類的方法
/*super.onAuthenticationSuccess(request, response, authentication);*/
//這裡可以根據實際情況,來確定是跳轉到頁面或者json格式。
//如果是返回json格式,那麼我們這麼寫
/*Map<String,String> map=new HashMap<>();
map.put("code", "200");
map.put("msg", "登入成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(map));
*/
//如果是要跳轉到某個頁面的,比如我們的那個whoim的則
new DefaultRedirectStrategy().sendRedirect(request, response, "/login-success");
return;
}
}
//登入失敗的
@Component("myAuthenticationFailHander")
public class MyAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// TODO Auto-generated method stub
logger.info("登入失敗");
//以Json格式返回
Map<String,String> map=new HashMap<>();
map.put("code", "201");
map.put("msg", "登入失敗");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(map));
}
}
程式碼完成之後,修改配置config類程式碼。
新增2個註解,自動注入
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler myAuthenticationFailHander;
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
//super.configure(http);
http
.formLogin().loginPage("/login").loginProcessingUrl("/login/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHander)
.permitAll() //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
}
進行測試,我們先返回json格式的(登入成功和失敗的)
改成跳轉到預設頁面
改造4、新增許可權控制
之前的程式碼我們使用者的許可權沒有加以利用,現在我們新增許可權的用法。
之前的登入驗證通俗的說,就是來判斷你是誰(認證),而許可權控制就是用來確定:你能做什麼或者不能做什麼(許可權)
在講這個之前,我們簡單說下,對於一些資源不需要許可權認證的,那麼就可以在Config中新增 過濾條件,如:
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
//super.configure(http);
http
.formLogin().loginPage("/login").loginProcessingUrl("/login/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHander)
.permitAll() //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
.and()
.authorizeRequests()
.antMatchers("/index").permitAll() //這就表示 /index這個頁面不需要許可權認證,所有人都可以訪問
.anyRequest().authenticated()
.and()
.csrf().disable();
}
那麼我們直接訪問 /index 就不會跳轉到登入頁面,這樣我們就可以把一些不需要驗證的資源以這種方式過濾,比如圖片,指令碼,樣式檔案之類的。
我們先來看第一種:在編碼中寫死的。
那其實許可權控制也是通過這種方式來實現:
http
.formLogin().loginPage("/login").loginProcessingUrl("/login/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHander)
.permitAll() //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
.and()
.authorizeRequests()
.antMatchers("/index").permitAll()
.antMatchers("/whoim").hasRole("ADMIN") //這就表示/whoim的這個資源需要有ROLE_ADMIN的這個角色才能訪問。不然就會提示拒絕訪問
.anyRequest().authenticated() //必須經過認證以後才能訪問
.and()
.csrf().disable();
這個使用者的角色哪裡來,就是我們自己的UserDetailsService中返回的使用者資訊中的角色許可權資訊,這裡需要注意一下就是 .hasRole("ADMIN"),那麼給使用者的角色時就要用:ROLE_ADMIN
.antMatchers 這裡也可以限定HttpMethod的不同要求不同的許可權(用於適用於Restful風格的API).
如:Post需要 管理員許可權,get 需要user許可權,我們可以這麼個改造,同時也可以通過萬用字元來是實現 如:/user/1 這種帶引數的URL
.antMatchers("/whoim").hasRole("ADMIN")
.antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
.antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
Spring Security 的校驗的原理:左手配置資訊,右手登入後的使用者資訊,中間投票器。
從我們的配置資訊中獲取相關的URL和需要的許可權資訊,然後獲得登入後的使用者資訊,
然後經過:AccessDecisionManager 來驗證,這裡面有多個投票器:AccessDecisionVoter,(預設有幾種實現:比如:1票否決(只要有一個不同意,就沒有許可權),全票通過,才算通過;只要有1個通過,就全部通過。類似這種的。
WebExpressionVoter 是Spring Security預設提供的的web開發的投票器。(表示式的投票器)
Spring Security 預設的是 AffirmativeBased 只要有一個通過,就通過。
有興趣的可以 從FilterSecurityInterceptor這個過濾器入口,來檢視這個流程。
內嵌的表示式有:permitAll denyAll 等等。
每一個許可權表示式都對應一個方法。
如果需要同時滿足多個要求的,不能連寫如 ,我們有個URL需要管理員許可權也同時要限定IP的話,不能:.hasRole("ADMIN").hasIPAddress("192.168.1.1");
而是需要用access方法 .access("hasRole('ADMIN') and hasIpAddress('192.168.1.1')");這種。
那我們可以自己寫許可權表示式嗎? 可以,稍後。。。這些都是硬編碼的實現,都是在程式碼中寫入的,這樣的靈活性不夠。所以我們接下來繼續改造
改造5、記住我的功能Remeber me
本質是通過token來讀取使用者資訊,所以服務端需要儲存下token資訊
根據官方的文件,token可以通過資料庫儲存 資料庫指令碼
必須嚴格按照下面的格式來建立資料庫的表!!
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) NOT NULL,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL,
PRIMARY KEY (series)
);
然後,配置好token 的儲存 及資料來源 在SecurityConfig裡寫:
@Autowired
private DataSource dataSource; //是在application.properites
/**
* 記住我功能的token存取器配置
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
修改Security配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
//super.configure(http);
http
.formLogin().loginPage("/login").loginProcessingUrl("/login/form")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHander)
.permitAll() //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
.and()
.rememberMe()
.rememberMeParameter("remember-me").userDetailsService(myUserDetailsService)
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600)
.and()
.authorizeRequests()
.antMatchers("/hello1").permitAll()//表示/index這個請求不需要許可權認證,所有人都可以訪問
.antMatchers("/whoim").hasRole("ADMIN")//表示/whoim需要有ROLE_ADMIN許可權的人才可以訪問
.anyRequest().authenticated()
.and()
.csrf().disable();// //取消放置csrf攻擊的防護
/*關於上面的程式碼解釋:loginPage("/login")表示登入時跳轉的頁面,因為我們不需要登入認證,所以加一個.permitAll()方法
failureUrl=表示登入出錯的頁面
*/
}
登入之後 資料庫就會有一條資料
然後,服務重新啟動下,我們在看下直接訪問 /whoim 的話,就可以直接訪問了,不需要再登入了。
到此為止我們的Spring Securtiy 的基本用法已經改造完成了。