目录
- 前言
- Spring Boot Security 和Spring Secutrity的关系
- 简单的权限控制
- 进一步探究
- 准备工作
- 编写权限代码(解决三个问题)
- 第一个问题:框架是如何从数据库里拿到用户信息?
- 第二个问题:框架是如何拿到用户访问的api所对应的角色的?
- 第三个问题:框架是如何判断登录用户的角色有没有权限访问这个api的呢?
- 测试
前言
众所周知,如果要对Web资源进行保护,最好的办法莫过于Filter,要想对方法调用进行保护,最好的办法莫过于AOP。Spring Security核心是一系列Filter,对资源进行过滤,这篇文档写的蛮不错的: .
Spring Security包括两块内容,Authentication(认证)和 Authorization(授权,也叫访问控制)。简单来说,认证即你是谁,授权即你有用什么权限。”认证”是认证主体的过程,通常是指可以在应用程序中执行操作的用户、设备或其它系统。”授权”是指是否允许已认证的主体执行某一项操作。
Spring Boot Security 和Spring Secutrity的关系
在Spring Security框架中,主要包含两个jar,即spring-security-web依赖和spring-security-config依赖。代码如下:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.2.RELEASE</version>
</dependency>
SpringBoot对Spring Secutiry做了封装,仅仅是做了封装,并没有改动什么东西。需要引入spring-boot-starter-security起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
简单的权限控制
新建一个springboot项目,只要在pom文件里引入spring-boot-starter-security起步依赖,就使用默认的配置自动开启了安全校验功能。启动项目时,会生成一个默认的用户(user)和密码,密码会打印在命令行里。访问项目时,会先跳出一个登陆页面。
如果想要自定义用户名和密码,可以采用下面配置:spring.security.user.name 和spring.security.user.password.
官方文档参考
进一步探究
初步体验了spring boot security,框架已经做了大量的工作,设置什么都不需要做,仅仅简单的把依赖引入进来,像是打开了一个开关,就开启了安全校验功能.然而实际实际开发中应用场景要比这复杂的多.例如需要能够创建用户和权限,要求程序能够从数据库中读取用户,完成认证和访问控制.就需要做一些适应性改造.在这个过程中,仅仅为了使用的话,把握几个核心问题,背后的原理可以日后探究.
第一个问题:框架是如何从数据库里拿到用户信息?
第二个问题:框架是如何拿到用户访问的api所对应的角色的?
第三个问题:框架是如何判断登录用户的角色有没有权限访问这个api的呢?
先看一张图,了解权限控制的整个流程:
准备工作
使用mysql数据库,使用jpa做持久层交互。不知道如何配置的可以参考我的这篇博文:地址 需要创建5张表用于存储用户(user),角色(role),权限(permission),用户角色关系表(user_role),角色权限关系表(role_permission).
表结构:
- 实体类父类:
@SuppressWarnings("serial")
@MappedSuperclass
@Data
public abstract class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_date", columnDefinition = "datetime")
@CreatedDate
private Date createdDate;
@Column(name = "update_date", columnDefinition = "datetime")
@LastModifiedDate
private Date updateDate;
- 创建用户类:
@SuppressWarnings("serial")
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user")
@EntityListeners(AuditingEntityListener.class)
public class User extends AbstractEntity {
@NotNull
@Column(name = "name", columnDefinition = "varchar(60) not null comment 'name'")
private String name;
@Column(name = "age", columnDefinition = "tinyint(1) not null comment 'age'")
private Integer age;
@Column(name = "password", columnDefinition = "varchar(60) not null comment 'password'")
private String password;
@NotNull
@Column(name = "phone_number", columnDefinition = "varchar(11) not null comment 'phone_number'", unique = true)
private String phoneNumber;
```
- 创建角色类
@SuppressWarnings("serial")
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "role")
@EntityListeners(AuditingEntityListener.class)
public class Role extends AbstractEntity {
@NotNull
@Column(name = "name", columnDefinition = "varchar(100) not null comment 'name'")
private String name;
}
- 创建权限类
@SuppressWarnings("serial")
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "permission")
@EntityListeners(AuditingEntityListener.class)
public class Permission extends AbstractEntity {
@NotNull
@Column(name = "name", columnDefinition = "varchar(100) not null comment 'name'")
private String name;
@Column(name = "url", columnDefinition = "varchar(255) not null comment 'resource path'")
private String url;
@Column(name = "pid", columnDefinition = "int not null comment 'pid'")
private Integer pid;
}
- 创建用户角色表
@SuppressWarnings("serial")
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "user_role")
@EntityListeners(AuditingEntityListener.class)
public class UserRole extends AbstractEntity {
@NotNull
@Column(name = "user_id", columnDefinition = "int not null comment 'user id'")
private Long userId;
@NotNull
@Column(name = "role_id", columnDefinition = "int not null comment 'role id'")
private Long roleId;
}
- 创建角色权限表
@SuppressWarnings("serial")
@Entity
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "role_permission")
@EntityListeners(AuditingEntityListener.class)
public class RolePermission extends AbstractEntity {
@NotNull
@Column(name = "role_id", columnDefinition = "int not null comment 'role id'")
private Long roleId;
@NotNull
@Column(name = "permission_id", columnDefinition = "int not null comment 'permission id'")
private Long permissionId;
}
- UserDetails接口是实现Spring Security的核心接口。getUsername是UserDetails里的方法,也可以返回别的信息,如邮箱,手机号码等。getAuthorities返回权限信息。有些人习惯直接使User实体类实现UserDetails接口,这样在这个类里面不仅要写users表的属性,还要重写UserDetails的方法,耦合度较高。User实体类原本只用来与数据库形成ORM映射,现在却要为框架提供其他功能。新建一个UserDto类,实现UserDetails接口
@SuppressWarnings("serial")
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
public class UserDto extends DeepflowAbstractDto implements UserDetails {
private String name;
private String password;
private Integer age;
private String phoneNumber;
private List<RoleDto> authorities;
@Override
public Collection<RoleDto> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return name;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- 同样的原因,创建一个RoleDto实现GrantedAuthority接口
@SuppressWarnings("serial")
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
public class RoleDto extends DeepflowAbstractDto implements GrantedAuthority {
private String name;
@Override
public String getAuthority() {
return name;
}
}
- 及创建对这些表进行CRUR操作的repository,service类
- 创建登录的controller 和 访问用户的api
@RestController
public class MainController {
@GetMapping("/")
public ModelAndView root() {
return new ModelAndView("index");
}
@GetMapping("/index")
public ModelAndView index() {
return new ModelAndView("index");
}
@RequestMapping(value = "/login", method = { RequestMethod.GET, RequestMethod.POST })
public ModelAndView login(HttpServletRequest request) {
return new ModelAndView("login");
}
@PostMapping("/login-error")
public ModelAndView loginError(Model model) {
ModelAndView modelAndView = new ModelAndView("login");
modelAndView.addObject("loginError", true);
return modelAndView;
}
@GetMapping("/401")
public ModelAndView accessDenied() {
return new ModelAndView("401");
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@Inject
private UserService userService;
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findOne(id);
}
@GetMapping("/phonenumber")
public List<UserDto> getUser(@RequestParam("phonenumber") List<String> phoneNumber) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.print(auth);
return userService.findUserByPhoneNumbers(phoneNumber);
}
@PostMapping()
public UserDto create(@RequestBody UserDto userDto) {
return userService.create(userDto);
}
@PutMapping()
public UserDto update(@RequestBody UserDto userDto) {
return userService.update(userDto);
}
@GetMapping("/list")
public List<UserDto> findAll(@RequestParam(name = "search", required = false) String search, Pageable pageable) {
return userService.search(search, pageable);
}
}
- 创建处理页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<p th:if="${loginError}" class="error">用户名或密码错误</p>
<form th:action="@{/login}" method="post">
<label for="username">用户名</label>:
<input type="text" id="username" name="username" autofocus="autofocus" />
<br/>
<label for="password">密 码</label>:
<input type="password" id="password" name="password" />
<br/>
<input type="submit" value="登录" />
</form>
<p><a href="/index" th:href="@{/index}"></a></p>
</body>
</html>
- 401.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>401 page</title>
</head>
<body>
<div>
<div>
<h2>权限不够</h2>
<p>拒绝访问!</p>
</div>
</div>
</body>
</html>
- 启动项目后,需要往数据库里插入一些测试数据
INSERT INTO user (id, name, password, age, phone_number) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e', 20, "15026773423");
INSERT INTO user (id, name, password, age, phone_number) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e', 21, "15026772323");
INSERT INTO role (id, name) VALUES (1,'USER');
INSERT INTO role (id, name) VALUES (2,'ADMIN');
INSERT INTO permission (id, url, name, pid) VALUES (1,'/api/users/phonenumber','phonenumber',0);
INSERT INTO permission (id, url, name, pid) VALUES (2,'/api/users','admin',0);
INSERT INTO permission (id, url, name, pid) VALUES (3,'/api/users/list','admin',0);
INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 3);
编写权限代码(解决三个问题)
编写权限代码需要解决几个问题:
第一个问题:框架是如何从数据库里拿到用户信息?
----重写UserDetailsService接口,重写loadUserByUsername类,这个类的作用是什么呢?这部分是由自己写代码拿到用户信息再返回给框架的。根据用户输入的用户名查找出该用户,并且根据用户id,查找出用户绑定的角色。
```
@Component
public class DeepflowUserDetailsService implements UserDetailsService {
@Inject
private UserService userService;
@Inject
private UserRoleService userRoleService;
@Inject
private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
UserDto userDto = userService.findUserByName(userName);
if (null != userDto) {
List<Long> roleIds = userRoleService.findByUserId(userDto.getId())//
.stream()//
.map(UserRoleDto::getRoleId)//
.collect(Collectors.toList());
List<RoleDto> roleDtos = roleService.findByIds(roleIds);
userDto.setAuthorities(roleDtos);
}
return userDto;
}
}
```
第二个问题:框架是如何拿到用户访问的api所对应的角色的?
----继承FilterInvocationSecurityMetadataSource接口,重写getAttributes方法 .
```
@Component
public class DeepflowSecuritySourceService implements FilterInvocationSecurityMetadataSource {
@Inject
private RolePermissionService rolePermissionService;
@Inject
private RoleService roleService;
@Inject
private PermissionService permissionService;
/**
* 每一个资源所需要的角色 Collection<ConfigAttribute>决策器会用到
*/
private static HashMap<String, Collection<ConfigAttribute>> map = null;
/**
* 返回请求的资源需要的角色
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if (map == null) {
loadResourceDefine();
}
// object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for (Iterator<String> it = map.keySet().iterator(); it.hasNext();) {
String url = it.next();
if (new AntPathRequestMatcher(url).matches(request)) {
return map.get(url);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
/**
* 初始化 所有资源 对应的角色
*/
public void loadResourceDefine() {
map = new HashMap<>(16);
// 权限资源 和 角色对应的表 也就是 角色权限 中间表
rolePermissionService.findAll()//
.stream()//
.forEach(rolePermisson -> {
// 某个资源 可以被哪些角色访问
RoleDto roleDto = roleService.findOne(rolePermisson.getRoleId());
PermissionDto permissionDto = permissionService.findOne(rolePermisson.getPermissionId());
String url = permissionDto.getUrl();
String roleName = roleDto.getName();
ConfigAttribute role = new SecurityConfig(roleName);
if (map.containsKey(url)) {
map.get(url).add(role);
} else {
List<ConfigAttribute> list = new ArrayList<>();
list.add(role);
map.put(url, list);
}
});
}
}
```
第三个问题:框架是如何判断登录用户的角色有没有权限访问这个api的呢?
–继承AccessDecisionManager接口,重写decide方法。这里可以自己去定义用户的角色是否有访问api的权限。如果没有权限,则跳转到401页面
```
@Component
public class DeepflowAccessDecisionManager implements AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护对象的权限
*
* @param authentication
* 包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
* @param object
* 就是FilterInvocation对象,可以得到request等web资源
* @param configAttributes
* configAttributes是本次访问需要的权限
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if (configAttributes == null || configAttributes.size() <= 0) {
return;
} else {
String needRole;
for (Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext();) {
needRole = iter.next().getAttribute();
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (needRole.trim().equals(ga.getAuthority().trim())) {
return;
}
}
}
throw new AccessDeniedException("当前访问没有权限");
}
}
/**
* 表示此AccessDecisionManager是否能够处理传递的ConfigAttribute呈现的授权请求
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/**
* 表示当前AccessDecisionManager实现是否能够为指定的安全对象(方法调用或Web请求)提供访问控制决策
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
```
- 面自定义的权限验证策略如何起作用呢?
----继承AbstractSecurityInterceptor类
@Component
public class DeepflowFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setDeepflowAccessDecisionManager(DeepflowAccessDecisionManager deepflowAccessDecisionManager) {
super.setAccessDecisionManager(deepflowAccessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
- 核心配置(SecurityConfig):自定义一些web security 安全设置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Inject
private DeepflowUserDetailsService userService;
@Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// 校验用户
auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
// 对密码进行加密
@Override
public String encode(CharSequence charSequence) {
System.out.println(charSequence.toString());
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
// 对密码进行判断匹配
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals(encode);
return res;
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 匹配的这些资源允许所有人访问
.antMatchers("/login", "/login-error", "/401", "/css/**", "/js/**").permitAll()
.antMatchers("/api/roles/**").hasAnyRole("ADMIN")
.anyRequest().authenticated() // 剩下所有的请求,都要进行授权访问校验
.and()
.formLogin()//
.loginPage("/login") // 指令表单登录页面,否则使用默认的
.failureUrl("/login-error") // 同上
.and()//
.exceptionHandling()//
.accessDeniedPage("/401")//
.and()//
.logout()//
.logoutUrl("/logout")//
.logoutSuccessUrl("/login");
}
}
测试
项目启起来后,可以使用admin/123456 , user/123456,分别访问:http://localhost:8080/api/users/list和http://localhost:8080/api/users/phonenumber?phonenumber=15026773462效果