SpringSecurity可以使用@PreAuthorize和@PostAuthorize進行通路控制,下面進行說明。
項目使用Springboot2.3.3+SpringSecurtiy+Mbatis實作。
首先先引入pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 需要單獨添加thymeleaf的布局子產品 -->
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
項目中使用log4j2替代了logback日志系統,需要将log4j2的配置檔案進行配置。
第一步:實作SpringSecurity的配置
擴充WebSecurityConfigurerAdapter的配置類
/**
* @author MaLei
* @description: 建立一個WebSecurityConfig類,使其繼 承WebSecurityConfigurerAdapter
* 在給WebSecutiryConfig類中加上@EnableWebSecurity 注解後,便會自動被 Spring發現并注冊(檢視
* @EnableWebSecurity 即可看到@Configuration 注解已經存在
* @create 2020/7/14
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//開啟全局方法配置這個注解必須開啟否則@PreAuthorize等注解不生效
public class WebSecutiryConfig extends WebSecurityConfigurerAdapter {
//認證管理器配置方法可以配置定定義的UserDetailService和passwordEncoder。無需配置springboot2.3會自動注入bean
/* @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(UserDetailService)
.passwordEncoder(new BCryptPasswordEncoder());
}*/
//核心過濾器配置方法
//void configure(WebSecurity web)用來配置 WebSecurity。而 WebSecurity是基于 Servlet Filter用來配置 springSecurityFilterChain。而 springSecurityFilterChain又被委托給了 Spring Security 核心過濾器 Bean DelegatingFilterProxy。 相關邏輯你可以在 WebSecurityConfiguration中找到。一般不會過多來自定義 WebSecurity, 使用較多的使其ignoring()方法用來忽略Spring Security對靜态資源的控制.對于靜态資源的忽略盡量在此處設定,否則容易無限循環重新定向到登入頁面
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**", "/mylogin.html","/admin", "/favicon.ico");
}
//安全過濾器鍊配置方法
//void configure(HttpSecurity http)這個是我們使用最多的,用來配置 HttpSecurity。 HttpSecurity用于建構一個安全過濾器鍊 SecurityFilterChain。SecurityFilterChain最終被注入核心過濾器 。 HttpSecurity有許多我們需要的配置。我們可以通過它來進行自定義安全通路政策
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); 不能使用預設的驗證方式
//authorizeRequests()方法實際上傳回了一個 URL 攔截注冊器,我們可以調用它提供的
//anyanyRequest()、antMatchers()和regexMatchers()等方法來比對系統的URL,并為其指定安全
//政策
http.authorizeRequests()
.anyRequest().authenticated()
.and()
//formLogin()方法和httpBasic()方法都聲明了需要Spring Security提供的表單認證方式,分别返
//回對應的配置器
.formLogin()
//,formLogin().loginPage("/myLogin.html")指定自定義的登入
//頁/myLogin.html,同時,Spring Security會用/myLogin.html注冊一個POST路由,用于接收登入請求
//loginProcessingUrl("/login")指定的/login必須與表單送出中指向的action一緻
.loginPage("/mylogin.html").loginProcessingUrl("/logins").permitAll()
//表單中使用者名和密碼對應參數設定(預設為username和password),如果是預設值則不用設定下面的參數對應.
.usernameParameter("usernames").passwordParameter("passwords")
.successForwardUrl("/hello")
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(403);
String error=new String();
if (e instanceof BadCredentialsException ||
e instanceof UsernameNotFoundException) {
error="賬戶名或者密碼輸入錯誤!";
} else if (e instanceof LockedException) {
error="賬戶被鎖定,請聯系管理者!";
} else if (e instanceof CredentialsExpiredException) {
error="密碼過期,請聯系管理者!";
} else if (e instanceof AccountExpiredException) {
error="賬戶過期,請聯系管理者!";
} else if (e instanceof DisabledException) {
error="賬戶被禁用,請聯系管理者!";
} else {
error="登入失敗!";
}
httpServletResponse.getWriter().write("{\"message\":\""+error+"\"}");
}
})
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException {
resp.setContentType("application/json;charset=utf-8");
@Cleanup PrintWriter out = resp.getWriter();
out.write("{\"msg\":\"登出成功!\"}");
out.flush();
// out.close();
}
})
.permitAll()
.and()
//csrf()方法是Spring Security提供的跨站請求僞造防護功能,當我們繼承WebSecurityConfigurer
//Adapter時會預設開啟csrf()方法
.csrf().disable()
//隻有确實的通路失敗才會進入AccessDeniedHandler,如果是未登陸或者會話逾時等,不會觸發AccessDeniedHandler,而是會直接跳轉到登陸頁面
.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString("{\"message\":\"權限不足,請聯系管理者!\"}"));
out.flush();
out.close();
}
});
}
/**
* 增加密碼加密器,一旦增加,在驗證過程中security将使用密碼加密器進行加密對比,資料庫中如果存儲明文密碼,在
* UserDetailsService接口實作方法中,先加密密碼然後才能傳回UserDetails
* @return
*/
@Bean
PasswordEncoder passwordEncoder(){
//使用系統自帶密碼加密器也可以參考上一篇自己繼承PasswordEncoder接口寫編碼器
return new BCryptPasswordEncoder();
}
}
在配置檔案中,靜态資源、登入頁面等求情不需要登入賬戶即可通路,我放在了
public void configure(WebSecurity web) throws Exception
方法中。
第二步:實作UserDetails及UserDetailsService接口。
User類實作UserDetails接口
public class User implements Serializable, UserDetails {
private Long id;
private String username;
private String password;
private String name;
private Boolean enabled;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
private static final long serialVersionUID = 1L;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
//将目前賬戶的所屬角色進行配置
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list=new ArrayList<>();
//将目前使用者的配屬角色填入集合
Assert.notNull(roles,"角色集合為null");
for (Role r:roles){
list.add(new SimpleGrantedAuthority(r.getName()));
}
return list.size()>0?list:null;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", username=").append(username);
sb.append(", password=").append(password);
sb.append(", name=").append(name);
sb.append(", enabled=").append(enabled);
sb.append("]");
return sb.toString();
}
}
UserDetailService類實作UserDetailsService接口
/**
* @author MaLei
* @description: UserDetailService
* @create 2020/7/14
*/
@Component
@Slf4j
public class UserDetailService implements UserDetailsService {
@Autowired
UserMapper customerMapper;
@Override
@Transactional("firstTransactionManager")
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("登入賬号:{}",username);
//資料庫讀取賬戶,如果讀出來的密碼是明碼必須使用PasswordEncoder加密
User cust=customerMapper.selectByUserNameContainsRoles(username);
if(cust==null)
throw new UsernameNotFoundException("賬戶不存在");
return cust;
}
}
由于使用Mybatis實作的資料庫操作,具體的mapper實作就不貼出來了。
第三步在前段Controller的方法上使用@PreAuthorize和@PostAuthorize
@PreAuthorize是在方法執行前進行權限認證
@PostAuthorize是方法執行後在傳回前進行權限認證
@RestController
public class TestController {
@RequestMapping("/hello")
@PreAuthorize("hasPermission('/hello', 'read') or hasRole('ROLE_admin')")
public String hello() {
return "hello";
}
代碼@PreAuthorize(“hasPermission(’/hello’, ‘read’) or hasRole(‘ROLE_admin’)”)中也使用了hasRole(‘ROLE_admin’),這代表目前登入使用者隻要具有ROLE_admin這個角色,即可通路。hasRole(‘ROLE_admin’)中角色名稱可以寫成ROLE_admin也可簡寫admin,系統會自動判斷是否有ROLE_字首,沒有會自動加上。
hasPermission(’/hello’, ‘read’)系統不會自動處理,需要實作PermissionEvaluator接口進行處理。
第四步驟:實作PermissionEvaluator接口
/**
* @author MaLei
* @description: PermissionEvaluator接口實作類
* @create 2020/7/17
*/
@Configuration
public class MyPermissionEvaluator implements PermissionEvaluator {
@Autowired
MenuMapper menuMapper;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
boolean accessable = false;
if(authentication.getPrincipal().toString().compareToIgnoreCase("anonymousUser") != 0){
Menu menu= menuMapper.selectByRequestUrl(targetDomainObject.toString());
if(menu==null) return accessable;
List<Role> roles = menu.getRoles();
Iterator<Role> it=roles.iterator();
while(it.hasNext()) {
Role role=it.next();
for (GrantedAuthority authority : authentication.getAuthorities()) {
if(role.getName().equals(authority.getAuthority())){
accessable=true;
}
}
}
}
return accessable;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission)
方法是處理
@PreAuthorize(“hasPermission(’/hello’, ‘read’) or hasRole(‘ROLE_admin’)”)
注解中hasPermission(’/hello’, ‘read’) 傳入的資訊的,
hasPermission方法中Authentication authentication參數代表登入使用者及權限資訊
Object targetDomainObject參數代表hasPermission(’/hello’, ‘read’) 中第一個參數"/hello"
Object permission參數代表hasPermission(’/hello’, ‘read’)中第二個參數"read"
在高版本的springboot中,實作了PermissionEvaluator接口後,隻要将實作類加上@Configuration系統會自動進行注冊到容器。
如果低版本springboot中,自定義的PermissionEvaluator實作類後不生效,需要在自己實作的WebSecutiryConfig配置類中聲明一個@Bean,形式如下:
/**
* 注入自定義PermissionEvaluator
*/
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler(){
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
//将自己實作的PermissionEvaluator接口實作類加入處理器
handler.setPermissionEvaluator(new MyPermissionEvaluator());
return handler;
}
通過以上配置就實作了使用@PreAuthorize、@PostAuthorize等注解來進行權限限制,本例中設定了所有請求必須登入後才能通路,如果一個賬戶登入了,但是請求某一資源後,該資源方法上未加上@PreAuthorize或@PostAuthorize注解,則代表該資源無需通路權限,可以直接通路,這點要注意。