一、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();
}
}
@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”;
}
html頁面是使用 thymeleaf 模板引擎的,這裏就不詳細講解了。
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>
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>
<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";
}
}
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");
}
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
我們也照樣,這是把用戶名改成 admin 密碼改成 123456 roles是該用戶的角色,我們後面再細說。
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("123456").roles("USER");
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("123456").roles("USER");
}
還有種方法 就是 重寫 另外一種configure(AuthenticationManagerBuilder auth) 方法,這個和上面那個方法的作用是一樣的。選其一就可。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth
.inMemoryAuthentication()
.withUser("admin").password("123456").roles("USER")
.and()
.withUser("test").password("test123").roles("ADMIN");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth
.inMemoryAuthentication()
.withUser("admin").password("123456").roles("USER")
.and()
.withUser("test").password("test123").roles("ADMIN");
}
程序運行起來,這時用我們自己的用戶名和密碼 輸入 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接口
代碼如下:
public 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;
}
}
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 null;
}
}
// TODO Auto-generated method stub
//這裏可以通過數據庫來查找到實際的用戶信息,這裏我們先模擬下,後續我們用數據庫來實現
if(username.equals("admin"))
{
//假設返回的用戶信息如下;
UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);
return userInfo;
}
return null;
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
//這裏可以可以通過username(登錄時輸入的用戶名)然後到數據庫中找到對應的用戶信息,並構建成我們自己的UserInfo來返回。
return null;
}
}
// TODO Auto-generated method stub
//這裏可以通過數據庫來查找到實際的用戶信息,這裏我們先模擬下,後續我們用數據庫來實現
if(username.equals("admin"))
{
//假設返回的用戶信息如下;
UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);
return userInfo;
}
return null;
View Code
到這裏爲止,我們自己定義的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.getPrincipal();// 這個是表單中輸入的密碼;
// 這裏構建來判斷用戶是否存在和密碼是否正確
UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 這裏調用我們的自己寫的獲取用戶的方法;
if (userInfo == null) {
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;
}
}
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.getPrincipal();// 這個是表單中輸入的密碼;
// 這裏構建來判斷用戶是否存在和密碼是否正確
UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 這裏調用我們的自己寫的獲取用戶的方法;
if (userInfo == null) {
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;
}
}
View Code
到此爲止,我們的用戶信息的獲取,校驗部分已經完成了。接下來要讓它起作用,則我們需要在配置文件中修改,讓他起作用。回到我的SecurityConfig代碼文件,修改如下:
1、注入我們自己的AuthenticationProvider
2、修改配置的方法:
@Autowired
private AuthenticationProvider provider; //注入我們自己的AuthenticationProvider
@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");
}
@Autowired
private AuthenticationProvider provider; //注入我們自己的AuthenticationProvider
@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");
}
View Code
現在重新運行程序,則需要輸入用戶名爲 admin 密碼是123456之後,才能正常登錄了。
爲了方便測試,我們調整添加另一個控制器 /whoim 的代碼 ,讓他返回當前登錄的用戶信息,前面說了,他是存在SecurityContextHolder 的全局變量中,所以我們可以這樣獲取
@RequestMapping("/whoim")
public Object whoIm()
{
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
@RequestMapping("/whoim")
public Object whoIm()
{
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
View Code
我們運行,直接反問 /whoim ,則直接跳轉到登錄頁面,我們驗證過之後,再訪問此url,結果如下:
到這裏,我們自定義的登錄已經成功了。
改造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, "/whoim");
}
}
@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, "/whoim");
}
}
//登錄失敗的
@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));
}
}
@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));
}
}
View Code
代碼完成之後,修改配置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();
}
@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();
}
View Code
進行測試,我們先返回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();
}
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();
}
View Code
那麼我們直接訪問 /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();
.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();
View Code
這個用戶的角色哪裏來,就是我們自己的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’)”);這種。
那我們可以自己寫權限表達式嗎? 可以,稍後。。。這些都是硬編碼的實現,都是在代碼中寫入的,這樣的靈活性不夠。所以我們接下來繼續改造
改造4、添加基於RBAC(role-Based-access control)權限控制
這個大家可以去百度一下,一般都是由 3個部分組成,一個是用戶,一個是角色 ,一個是資源(菜單,按鈕),然後就是 用戶和角色的關聯表,角色和資源的關聯表
核心就是判斷當前的用戶所擁有的URL是否和當前訪問的URL是否匹配。
首先我們自己提供一個判斷的接口和實現,代碼如下:
/**
* 返回權限驗證的接口
*
*
*/
public interface RbacService {
boolean hasPermission(HttpServletRequest request,Authentication authentication);
}
@Component("rbacService")
public class RbacServiceImpl implements RbacService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
boolean hasPermission = false;
if (principal instanceof UserDetails) { //首先判斷先當前用戶是否是我們UserDetails對象。
String userName = ((UserDetails) principal).getUsername();
Set<String> urls = new HashSet<>(); // 數據庫讀取 //讀取用戶所擁有權限的所有URL
urls.add("/whoim");
// 注意這裏不能用equal來判斷,因爲有些URL是有參數的,所以要用AntPathMatcher來比較
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
}
return hasPermission;
}
}
public interface RbacService {
boolean hasPermission(HttpServletRequest request,Authentication authentication);
}
@Component("rbacService")
public class RbacServiceImpl implements RbacService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
boolean hasPermission = false;
if (principal instanceof UserDetails) { //首先判斷先當前用戶是否是我們UserDetails對象。
String userName = ((UserDetails) principal).getUsername();
Set<String> urls = new HashSet<>(); // 數據庫讀取 //讀取用戶所擁有權限的所有URL
urls.add("/whoim");
// 注意這裏不能用equal來判斷,因爲有些URL是有參數的,所以要用AntPathMatcher來比較
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
}
return hasPermission;
}
}
View Code
然後在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()
.authorizeRequests()
// .antMatchers("/index").permitAll()
// .antMatchers("/whoim").hasRole("ADMIN")
// .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
// .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
.anyRequest().access("@rbacService.hasPermission(request,authentication)") //必須經過認證以後才能訪問
.and()
.csrf().disable();
}
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()
// .antMatchers("/whoim").hasRole("ADMIN")
// .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
// .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
.anyRequest().access("@rbacService.hasPermission(request,authentication)") //必須經過認證以後才能訪問
.and()
.csrf().disable();
}
View Code
其中 @rbacService 就是我們自己聲明的bean,在RbacServiceImpl實現類的頭部註解中。
改造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)
);
(
username VARCHAR(64) NOT NULL,
series VARCHAR(64) NOT NULL,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL,
PRIMARY KEY (series)
);
然後,配置好token 的存儲 及數據源
@Autowired
private DataSource dataSource; //是在application.properites
/**
* 記住我功能的token存取器配置
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Autowired
private DataSource dataSource; //是在application.properites
/**
* 記住我功能的token存取器配置
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
View Code
修改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(userDetailsService)
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)
.and()
.authorizeRequests()
// .antMatchers("/index").permitAll()
// .antMatchers("/whoim").hasRole("ADMIN")
// .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
// .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
.anyRequest().access("@rbacService.hasPermission(request,authentication)") //必須經過認證以後才能訪問
.and()
.csrf().disable();
@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(userDetailsService)
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)
.and()
.authorizeRequests()
// .antMatchers("/index").permitAll()
// .antMatchers("/whoim").hasRole("ADMIN")
// .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
// .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
.anyRequest().access("@rbacService.hasPermission(request,authentication)") //必須經過認證以後才能訪問
.and()
.csrf().disable();
View Code
登錄之後 數據庫就會有一條數據
然後,服務重新啓動下,我們在看下直接訪問 /whoim 的話,就可以直接訪問了,不需要再登錄了。