springboot 与 security 保护 web 安全
一. 简述
做 web
项目,权限的事情就常常伴随着左右,比如某个用户有没有资格访问这个资源,常用的权限框架一般有 shrio
和 spring-security
。一般来说,很多公司会选择前者,因为会稍微轻量级(其实我为了写这篇文章稍微学习一下 spring-security
也有这种感觉,spring
公司把用户权限等等都已经提供了接口,不利于我们自己定制化,有好有坏吧,熟悉的话快速开发是个不错的选择)。当然现在我有种比较喜欢 spring
的全家桶的感觉,毕竟开发起来很面向对象。而且自家的产品,跟自己搭配起来,都会很简单的就起来了。
OK,简单的搭建没什么意义,所以这里我提前用了下 jpa
,简单的示例。 业务情景是这样子的:我数据库里面有一堆资源,但是有规定,不同角色只能请求不同的资源,当你的角色不匹配,请求的时候会被拒绝。用户和角色我就没分出来表了,放在同一张表,目的是快速的进行演示。 示例代码:web-security
二. 开发
1. mvn
依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <dependencies>
<!-- web-starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<!-- security-starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
<!-- jpa-starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
<!-- mysql 依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
</dependencies>
|
2. 定义资源
OK,因为需要类似于实战的东西,在资源定义这里我使用了 Spring
的钩子 + Spring-data-jpa
来辅助实战。
2.1 两个资源(均省略了 getter
和 setter
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Entity(name = "security_user") public class User {
@Id @GeneratedValue(strategy= GenerationType.AUTO) @Column(name = "user_id") private Long id;
@Column(name = "user_name", length = 13) private String name;
@Column(name = "user_role", length = 20) private String role;
public User() { }
public User(String name, String role) { this.name = name; this.role = role; } }
@Entity(name = "security_resource") public class Resource {
@Id @GeneratedValue(strategy= GenerationType.AUTO) @Column(name = "res_id") private Long id;
@Column(name = "res_role", length = 20) private String requireRole;
@Column(name = "res_content") private String content;
public Resource() { }
public Resource(String requireRole, String content) { this.requireRole = requireRole; this.content = content; } }
|
2.2 两个资源对应的仓库
1 2 3 4 5 6 7 8 9 10 11 12
| @Repository public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String name);
} @Repository public interface ResourceRepository extends JpaRepository<Resource, Long> {
List<Resource> findByRequireRole(String requireRols);
}
|
2.3 项目启动初始化
这个数据库包含三个用户以及对应角色可以访问到的一共十个资源,全部在启动的时候写入数据库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| @Configuration public class InitDbConfig {
private UserRepository userRepository; private ResourceRepository resourceRepository;
@Autowired public InitDbConfig(UserRepository userRepository, ResourceRepository resourceRepository) { this.userRepository = userRepository; this.resourceRepository = resourceRepository; }
@PostConstruct public void initUser() { User user1 = new User("WEIDAN", "admin"); User user2 = new User("XIAOMING", "vip"); User user3 = new User("XIAOHONG", "customer"); List<User> addUser = new ArrayList<>(3); addUser.add(user1); addUser.add(user2); addUser.add(user3); userRepository.saveAll(addUser); }
@PostConstruct public void initResource() { Resource resource1 = new Resource("admin", UUID.randomUUID().toString()); Resource resource2 = new Resource("admin", UUID.randomUUID().toString()); Resource resource3 = new Resource("vip", UUID.randomUUID().toString()); Resource resource4 = new Resource("admin", UUID.randomUUID().toString()); Resource resource5 = new Resource("vip", UUID.randomUUID().toString()); Resource resource6 = new Resource("admin", UUID.randomUUID().toString()); Resource resource7 = new Resource("vip", UUID.randomUUID().toString()); Resource resource8 = new Resource("admin", UUID.randomUUID().toString()); Resource resource9 = new Resource("admin", UUID.randomUUID().toString()); Resource resource10 = new Resource("customer", UUID.randomUUID().toString()); List<Resource> resourceList = new ArrayList<>(10); resourceList.add(resource1); resourceList.add(resource2); resourceList.add(resource3); resourceList.add(resource4); resourceList.add(resource5); resourceList.add(resource6); resourceList.add(resource7); resourceList.add(resource8); resourceList.add(resource9); resourceList.add(resource10); resourceRepository.saveAll(resourceList); }
}
|
2.4 项目配置
既然使用到了数据库,那么项目配置就必不可少了。
1 2 3 4 5 6 7 8 9 10 11
| spring: jpa: hibernate: ddl-auto: create-drop show-sql: true database-platform: org.hibernate.dialect.MySQL5InnoDBDialect datasource: url: jdbc:mysql://localhost:3306/security_db driver-class-name: com.mysql.jdbc.Driver username: root password: root
|
3. 配合 spring-security
开发业务层
3.1 用户业务层
这个业务层不需要我们定义接口,用于实现 Spring
提供的 UserDetailsService
接口即可。当然一般来说我们的用户层业务不会只有他那么一个方法,所以如若需要还是要定义我们自己的用户业务层接口的。 UserDetailsService
这个类,只有一个方法 loadUserByUsername
,我们需要实现它,读取我们数据库用户的数据然后返回 Spring
定义的 UserDetails
,当然了,也可以使用我们自己的类来集成这个 UserDetails
。我想这一步可能就是大家所说的 spring-security
不轻量级的地方吧。因为权限模型都定义了,我们还需要根据他的需要来将我们的数据转换到他的类去。 在这里为了方便我把所有的用户的密码都设置成 123
了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| package cn.liweidan.springboot.security.service;
import cn.liweidan.springboot.security.dbo.User; import cn.liweidan.springboot.security.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.List; import java.util.Objects;
@Service public class UserDetailServiceImpl implements UserDetailsService {
private UserRepository userRepository;
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired public UserDetailServiceImpl(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { this.userRepository = userRepository; this.bCryptPasswordEncoder = bCryptPasswordEncoder; }
@Override public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException { User user = userRepository.findByName(name); if (Objects.isNull(user)) { throw new UsernameNotFoundException(name + " is not found"); } List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(); grantedAuthorityList.add(new SimpleGrantedAuthority(user.getRole()));
return new org.springframework.security.core.userdetails.User(user.getName(), bCryptPasswordEncoder.encode("123"), grantedAuthorityList); }
}
|
3.2 资源业务层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Service public class ResourceService {
private ResourceRepository resourceRepository;
@Autowired public ResourceService(ResourceRepository resourceRepository) { this.resourceRepository = resourceRepository; }
@PreAuthorize("hasAuthority('vip')") public List<Resource> getVipResource() { return resourceRepository.findByRequireRole("vip"); }
}
|
3.3 资源控制器
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RequestMapping("resource") public class ResourceEndpoint {
@Autowired private ResourceService resourceService;
@GetMapping("vip") public List<Resource> getVipResource() { return resourceService.getVipResource(); }
}
|
4. 测试
启动项目,请求 http://localhost:8080/resource/vip
会发现请求被重定向到一个内置的登陆页面(可定制的):
我使用 WEIDAN
123
来登录(并不具备 vip 的角色)然后请求 vip 的资源。发现被拒绝访问了(这个页面可以通过之前所说的 ControllerAdvise
来定制):
1 2 3 4 5 6
| Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sat Dec 01 09:11:58 CST 2018 There was an unexpected error (type=Forbidden, status=403). Forbidden
|
OK,接下来使用一个 XIAOMING
123
来登录(具备 vip 角色),然后请求 vip 资源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| [ { "id": 6, "requireRole": "vip", "content": "963d08cd-4ddb-4601-865a-9d91470bad28" }, { "id": 8, "requireRole": "vip", "content": "574353a7-5ec8-4383-b5b2-2b4435220069" }, { "id": 10, "requireRole": "vip", "content": "d019d923-e267-4a18-a632-5cbecc04c012" } ]
|
三. 总结
Spring-security
提供了整套权限模型,如若真的计划使用来架构权限的话,需要了解有哪些类。Spring
依然提供了一些可以控制权限的注解类,这些类需要加在方法上面,初步感觉,可能有点不太灵活。在未来的微服务架构中我更希望是通过路由来控制接口的访问权限,而项目服务中开发就不再关注,降低耦合性,也能尽量防止在项目服务中开发错误的局面。