一.前言
1.介绍
- 上文主要介绍了后端使用Spring Security对API进行保护
- 本文主要介绍Spring Security使用数据库存储角色和权限,并在此情况下进行登录操作,同时介绍了记住我操作
2.项目例子
- 此文章用到的例子在spring-boot项目中,传送门
- 此篇文章用到项目模块:spring-boot-security-login
- 还有更多:spring-cloud项目
3.概述
- 使用数据库存储用户,角色和权限,代码中使用jpa进行数据库访问
- 自定义UserDetailsService用于登陆时获取用户信息
- 添加rememberMe记住我的功能
- sql文件在项目模块中login.sql
- 基于上篇后端使用Spring Security对API进行保护添加登陆的新功能
二.Spring Security用户,角色和权限
1. 数据库表设计
- 用户,角色,权限,之间多对多关系
- 用户实体类,包含角色
@Entity
@Table(name = "user_account")
public class User {
@Id
@Column(unique = true, nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String email;
@Column(length = 60)
private String password;
private boolean enabled;
private boolean isUsing2FA;
private String secret;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "users_roles", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private Collection<Role> roles;
- 角色实体类,包含权限
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToMany(mappedBy = "roles")
private Collection<User> users;
@ManyToMany
@JoinTable(name = "roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id"))
private Collection<Privilege> privileges;
private String name;
- 权限实体类
@Entity
public class Privilege {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@ManyToMany(mappedBy = "privileges")
private Collection<Role> roles;
2. 自定义获取登陆时用户数据
- 实现UserDetailsService接口
- 重写loadUserByUsername方法,返回User给security进行验证
- 使用userRepository从数据库中获取用户以及权限
- User构造方法,依次是账号,密码,账号是否过期,证书是否过期,是否锁定账号,权限集合
@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
public MyUserDetailsService() {
super();
}
// API
@Override
public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException {
try {
final User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException("No user found with username: " + email);
}
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, getAuthorities(user.getRoles()));
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
// UTIL
private final Collection<? extends GrantedAuthority> getAuthorities(final Collection<Role> roles) {
return getGrantedAuthorities(getPrivileges(roles));
}
private final List<String> getPrivileges(final Collection<Role> roles) {
final List<String> privileges = new ArrayList<>();
final List<Privilege> collection = new ArrayList<>();
for (final Role role : roles) {
collection.addAll(role.getPrivileges());
}
for (final Privilege item : collection) {
privileges.add(item.getName());
}
return privileges;
}
private final List<GrantedAuthority> getGrantedAuthorities(final List<String> privileges) {
final List<GrantedAuthority> authorities = new ArrayList<>();
for (final String privilege : privileges) {
authorities.add(new SimpleGrantedAuthority(privilege));
}
return authorities;
}
}
3. 将自定义用户源放入到spring security配置中
- 将MyUserDetailsService 放入到spring security配置中
@Autowired
private MyUserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
4. 使用PreAuthorize标识访问接口需要使用的权限
- 开启PreAuthorize支持,将prePostEnabled设置为true
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityJavaConfig extends WebSecurityConfigurerAdapter {
- controller中DeleteMapping需要DELETE_USER权限,而GetMapping需要GET_USER权限
@RestController
@RequestMapping("/api/user")
public class BusinessController {
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('DELETE_USER')")
public String deleteUser(@PathVariable Long id){
return "delete user success by user id :"+id;
}
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('GET_USER')")
public User getUser(@PathVariable Long id){
User user = new User();
user.setId(id);
return user;
}
}
5. 验证
- 没有登录,访问接口,返回401 Unauthorized
@Test
public void notLogin() {
ResponseEntity<String> entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE,
new HttpEntity<Void>(loginHeaders), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
- 账号test2@test.com账号没有调用删除用户接口,返回403 Forbidden
@Test
public void noHasDeleteUserAuthority() {
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.set("username", "test2@test.com");
form.set("password", "123456");
login(form);
ResponseEntity<String> entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE,
new HttpEntity<Void>(loginHeaders), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
三.记住我
1. 在spring security中设置记住我
- 在configure(HttpSecurity http)中添加rememberMe
- 添加过期时间为24小时
.and().rememberMe().tokenValiditySeconds(60*60*24);
2. 将servlet session有效活动时间修改为1分钟
- 默认servlet session有效活动时间为30分钟
- 在application.yml中设置
server:
port: 8081
tomcat:
uri-encoding: UTF-8
servlet:
session:
timeout: 1m
3. 介绍
- 当登陆的时候,使用remember-me:true,此时返回值会生成remember-me的cookie值,存储了账号的md5加密和过期时间,如下
remember-me=dGVzdCU0MHRlc3QuY29tOjE1NjY4MzE1NjQzNjQ6YjNiODE4YzhhZTgwMDMwNzY4NDE2YTE1ZDU5YmZmOTg; Max-Age=30000; Expires=Mon, 26-Aug-2019 14:59:24 GMT; Path=/; HttpOnly
- 同时登陆,返回登陆cookie凭证如下
JSESSIONID=1DD1D2BDFDA29944732B394F26F73D7E; Path=/; HttpOnly
- 因为我们设置了session的过期时间为1分钟后,在登陆一分钟后JSESSIONID访问业务接口会失败
- 而使用remember-me的cookie还可以访问业务接口
4. 验证
- 我们将返回header中的cookie存储到文件中
- 当一分钟后使用JSESSIONID访问业务接口失败
- 使用remember-me的cookie访问业务接口成功
@Test
public void deleteOneMinuteLaterByJsessionId() throws InterruptedException, IOException {
HttpHeaders loginHeaders = getHttpHeaders(1);
//sleep one minute until session expired
Thread.sleep(60000L);
ResponseEntity<String> entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE,
new HttpEntity<Void>(loginHeaders), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void deleteByRememberMeCookie() throws IOException {
HttpHeaders loginHeaders = getHttpHeaders(0);
ResponseEntity<String> entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE,
new HttpEntity<Void>(loginHeaders), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
四.总结
- 本文主要介绍Spring Security使用数据库存储角色和权限,并在此情况下进行登录操作,同时介绍了记住我操作
上一篇:Spring Security 对Rest风格API的保护