在看完Shiro框架文章时,我们大致应该知道了,了解了安全框架应该去做哪些事情,并且随着微服务架构的发展,Spring Security框架在安全框架的地位也发展的越来越好,所以,了解Spring Security框架的使用还是很有必要的。这篇博客是用来巩固自己的学习内容,博客来源与:江南一点雨,所发的视频及他发的文章。
Spring Security框架能做什么呢
它和上一篇文章讲的Shiro一样,它用来做授权和认证相关功能
快速了解一下Spring Security的强大之处
- 首先创建一个Spring Boot项目,勾上一个 web依赖和一个Spring Security依赖后,创建一个Hello接口
如下图所示:
- 然后,直接启动项目,在启动项目过程中,我们会看到如下一行日志:
Using generated security password: ed9c6238-9afe-4204-9750-6528bb924e44
- 项目启动成功后,像平常一样去访问
http://localhost:8080/hello
接口,我们会发现,和平时不太一样了,平时都能够直接弹出一个hello的字符串,这次竟然弹出了一个登录界面,再说,我们也压根没有写登录界面呀!
不用说也知道,肯定是因为有了Spring Security依赖的关系了。那么我们该如何登录呢?
在登录页面,默认的用户名就是 user,默认的登录密码则是项目启动时控制台打印出来的密码,输入用户名密码之后,就登录成功了,登录成功后,我们就可以访问到 /hello 接口了。
我们可以一个知道:一个依赖就保护了所以接口,应该比Shiro框架要强大很多吧!
Spring Security结合数据库的完整配置
首先添加依赖
结合了Spring Data jpa,所以导入了下列依赖
<?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.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mark</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security</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-data-jpa</artifactId>
</dependency>
<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>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>
完成配置文件的配置
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url= jdbc:mysql:///test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.jpa.database=mysql
spring.jpa.database-platform=mysql
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
项目的目录结构展示图
创建实体类(共两个实体类)
- Role(角色类)
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity(name = "t_role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String nameZh;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
}
- User(用户类)
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Entity(name = "t_user")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
//这是多对多的一个注解,
@ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
private List<Role> roles;
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
可以看到这两个类当中有Spring data jpa 的相关注解,并且可以看到,我们写的User类它实现了一个UserDetails
类,因为Spring Security需要对User进行相关安全控制,所以必要满足Spring Security的相关方法。并且采用Spring Data Jpa 还会生成实体类相对应的表格
Dao层
import com.mark.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserDao extends JpaRepository<User,Long> {
User findUserByUsername(String username);
}
Service层
import com.mark.dao.UserDao;
import com.mark.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService {
@Autowired
UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}
可以看到,这个service ,实现了一个UserDetailsService
类,这个也是为了Spring Security的相关配置
在配置Spring Security 配置之前
先启动项目,由于配置了Spring Data Jpa 的关系,它会自动在数据库中生成两个表,为啥是三个表呢?,不是仅仅才两个实体类吗?,除了生成了两个实体类的表还生成了一个,两个实体类的关联表。再使用一个测试类来往数据库的表格插入数据。
import com.mark.dao.UserDao;
import com.mark.model.Role;
import com.mark.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class SecurityApplicationTests {
@Autowired
UserDao userDao;
@Test
void contextLoads() {
User u1 = new User();
u1.setUsername("ncu");
u1.setPassword("123");
u1.setAccountNonExpired(true);
u1.setAccountNonLocked(true);
u1.setCredentialsNonExpired(true);
u1.setEnabled(true);
List<Role> rs1 = new ArrayList<>();
Role r1 = new Role();
r1.setName("ROLE_admin");
r1.setNameZh("管理员");
rs1.add(r1);
u1.setRoles(rs1);
userDao.save(u1);
User u2 = new User();
u2.setUsername("MarkZQP");
u2.setPassword("123");
u2.setAccountNonExpired(true);
u2.setAccountNonLocked(true);
u2.setCredentialsNonExpired(true);
u2.setEnabled(true);
List<Role> rs2 = new ArrayList<>();
Role r2 = new Role();
r2.setName("ROLE_user");
r2.setNameZh("普通用户");
rs2.add(r2);
u2.setRoles(rs2);
userDao.save(u2);
}
}
数据库及运行结果截图:
下面是最重要的Spring Security的相关配置
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mark.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.io.PrintWriter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
// 权限的相关设计
// 可以看到,admin权限比user权限大,只有有了admin权限
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin") //只有有admin角色才能访问/admin/**接口
.antMatchers("/user/**").hasRole("user") //只有user角色才能访问/user/**接口
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/doLogin") // 登录界面提交时需要访问的接口"/doLogin"
.successHandler((req, resp, authentication) -> {
// 这是登录成功是所调的函数,返回相关的JSON数据
// 有了相关的JSON数据,前端就可以进行相关的处理
Object principal = authentication.getPrincipal();
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(principal));
out.flush();
out.close();
})
.failureHandler((req, resp, e) -> {
// 这是登录失败所调的函数,也是返回相关的JSON数据
// 有了相关的JSON数据,前端就可以进行相关的处理
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(e.getMessage());
out.flush();
out.close();
})
.permitAll()
.and()
.logout()
.logoutUrl("/logout") // 注销链接所调用的logout接口
.logoutSuccessHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("注销成功");
out.flush();
out.close();
})
.permitAll()
.and()
.csrf().disable().exceptionHandling()
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("尚未登录,请先登录");
out.flush();
out.close();
}
);
}
}
代码这么多,先来了解每一个函数对应的功能吧
- 首先,我们创建了一个
SecurityConfig
的类,它继承了WebSecurityConfigurerAdapter
这个类(这是因为它是Spring Security的相关配置),并且加上了@Configuration
这个注解让它成为了一个配置类。并且可以重写WebSecurityConfigurerAdapter
的相关方法以完成相关功能。 - 首先我们提供了一个 PasswordEncoder 的实例,因为目前的案例还比较简单,因此我暂时先不给密码进行加密,所以返回 NoOpPasswordEncoder 的实例即可。
- 第一个
configure
, 直接就一行代码auth.userDetailsService(userService)
,它完成的功能就是验证用户的账号密码。 - 第二个
configure
,不拦截"/js/**", "/css/**", "/images/**"
,这些个信息 - 第三个
configure
,有相关注解。
使用postman进行测试
- 直接访问hello接口
- 登录 nuc账号,它的角色为admin
登录成功后,在去访问接口/hello,/admin/hello,/user/hello接口都能访问成功,虽然在权限上设置了user下的接口需要user角色才能访问,但是在权限上给了其它的设置,admin角色可以访问user角色所能访问的。 - 登录 mark账号,它的角色为user
登录成功后,在去访问接口/hello,/user/hello接口都能访问成功,但是,/admin/hello接口,不能访问成功,因为有权限的设置
博客到这里就结束了,可能介绍的不够详细,不过— 江南一点雨— 这个大佬写的特别好,可以去看他的文章。