寫在前面
在前面的學習當中,我們對spring security有了一個小小的認識,接下來我們整合目前的主流框架springBoot,實現權限的管理。
在這之前,假定你已經了解了基於資源的權限管理模型。數據庫設計的表有 user 、role、user_role、permission、role_permission。
步驟:
默認大家都已經數據庫已經好,已經有了上面提到的表。(文末提供sql腳本下載)
第一步:在pom.xml文件中引入相關jar包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>pers.lbf</groupId>
<artifactId>springboot-spring-securioty-demo1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-spring-security-demo1</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第二步:修改application.yml文件,添加數據庫相關配置
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/secutiry_authority?useSSL=false&serverTimezone=GMT
username: root
password: root1997
driver-class-name: com.mysql.cj.jdbc.Driver
第三步:啟動項目
springboot已經給我們提供好了一個默認的username為“user”,其密碼可以在控制台輸出中得到。並且在springBoot的默認配置中,所有資源必須要通過認證后才能訪問
打開<http://127.0.0.1:8081/login 即可看到默認的登錄頁面。
第四步:添加配置類,覆蓋springBoot對spring security的默認配置
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/8/28 20:22
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userService;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用跨域保護
http.csrf().disable();
//配置自定義登錄頁
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.usernameParameter("username")
.passwordParameter("password");
//配置登出
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");
}
@Override
public void configure(WebSecurity webSecurity) throws Exception{
//忽略靜態資源
webSecurity.ignoring().antMatchers("/assents/**","/login.html");
}
}
關於EnableGlobalMethodSecurity注解的說明
@EnableGlobalMethodSecurity(securedEnabled=true)
開啟@Secured 注解過濾權限
@EnableGlobalMethodSecurity(jsr250Enabled=true)
開啟@RolesAllowed 注解過濾權限
@EnableGlobalMethodSecurity(prePostEnabled=true)
使用表達式時間方法級別的安全性 4個注解可用
@PreAuthorize 在方法調用之前,基於表達式的計算結果來限制對方法的訪問
@PostAuthorize 允許方法調用,但是如果表達式計算結果為false,將拋出一個安全性異常
@PostFilter 允許方法調用,但必須按照表達式來過濾方法的結果
@PreFilter 允許方法調用,但必須在進入方法之前過濾輸入值
第五步:編寫代碼,實現對User、role、permission的CRUD
5.1 編寫自己的user對象,實現spring security的UserDetails接口,並實現對User的查找操作
關於為什么要實現這個接口,大家可以參考我上一篇文章《Spring Security認證流程分析--練氣后期》。
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/8/28 22:14
*/
public class UserDO implements UserDetails {
private Integer id;
private String username;
private String password;
private Integer status;
private List<SimpleGrantedAuthority> authorityList;
@Override
public String toString() {
return "UserDO{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", status=" + status +
", authorityList=" + authorityList +
'}';
}
public List<SimpleGrantedAuthority> getAuthorityList() {
return authorityList;
}
public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
this.authorityList = authorityList;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setStatus(Integer status) {
this.status = status;
}
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorityList;
}
/**
* Returns the password used to authenticate the user.
*
* @return the password
*/
@Override
public String getPassword() {
return this.password;
}
/**
* Returns the username used to authenticate the user. Cannot return <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
@Override
public String getUsername() {
return this.username;
}
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
*
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
@Override
public boolean isAccountNonExpired() {
return this.status==1;
}
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
@Override
public boolean isAccountNonLocked() {
return this.status == 1;
}
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
@Override
public boolean isEnabled() {
return this.status==1;
}
}
關於用戶憑證是否過期、賬戶是否被鎖定大家可以自己實現一下
**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/8/28 22:17
*/
public interface IUserDao {
@Select("select * from sys_user u where u.username=#{name}")
UserDO findByName(String name);
}
5.2 編寫Role和Permission兩個實體類,並實現對其查找的Dao
RoleDO
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/1 20:51
*/
public class RoleDO implements Serializable {
private Integer id;
private String roleName;
private String roleDesc;
}
PermissionDO
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/1 21:27
*/
public class PermissionDO implements Serializable {
private Integer id;
private String permissionName;
private String permissionUrl;
private Integer parentId;
}
IRoleDao
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/1 20:53
*/
public interface IRoleDao {
@Select("select * from sys_role sr where sr.id in (select rid from sys_user_role where uid=#{userId})")
List<RoleDO> findByUserId(Integer userId);
}
IPermissionDao
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/9/1 21:30
*/
public interface IPermissonDao {
@Select("select * from sys_permission sp where sp.id in (select pid from sys_role_permission where rid=#{roleId})")
List<PermissionDO> findByRoleId(Integer roleId);
}
5.3 編寫UserService 實現UserDetailsService接口並實現loadUserByUsername方法
**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/8/28 22:16
*/
@Service("userService")
public class UserServiceImpl implements UserDetailsService {
@Autowired
private IUserDao userDao;
@Autowired
private IRoleDao roleDao;
@Autowired
private IPermissonDao permissonDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username == null){
return null;
}
UserDO user = userDao.findByName(username);
//加載權限
List<RoleDO> roleList = roleDao.findByUserId(user.getId());
List<SimpleGrantedAuthority> list = new ArrayList<> ();
for (RoleDO roleDO : roleList) {
List<PermissionDO> permissionListItems = permissonDao.findByRoleId(roleDO.getId());
for (PermissionDO permissionDO : permissionListItems) {
list.add(new SimpleGrantedAuthority(permissionDO.getPermissionUrl()));
}
}
user.setAuthorityList(list);
return user;
}
}
第六步:編寫一個測試接口
/**
* @author 賴柄灃 bingfengdev@aliyun.com
* @version 1.0
* @date 2020/8/27 20:02
*/
@RestController
@RequestMapping("/product")
public class TestController {
@GetMapping("/")
@PreAuthorize("hasAuthority('product:get')")
public String get() {
return "調用成功";
}
}
第七步 :使用postman進行測試
7.1登錄操作
登錄成果返回主頁
登錄失敗返回登錄頁面
7.2調用受保護的接口
有權限則調用成功
無權限返回403
大家可以實現一下對異常的攔截,給用戶返回一個友好的提示。
寫在最后
這是springBoot整合spring security單體應用的一個小demo。關於分布式的、使用JWT代替spring security 的csrf,並自定義認證器的例子將在我的下一篇文章中介紹。
代碼及sql腳本下載:https://github.com/code81192/art-demo/tree/master/springboot-spring-securioty-demo1