一.前言
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
roles; -
角色实体类,包含权限
@Entity public class Role { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ManyToMany(mappedBy = "roles") private Collection
users; @ManyToMany @JoinTable(name = "roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id")) private Collection privileges; private String name; -
权限实体类
@Entity public class Privilege { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @ManyToMany(mappedBy = "privileges") private Collection
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
roles) { return getGrantedAuthorities(getPrivileges(roles)); } private final List getPrivileges(final Collection roles) { final List privileges = new ArrayList<>(); final List 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 getGrantedAuthorities(final List privileges) { final List 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
entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE, new HttpEntity (loginHeaders), String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } - 账号[email protected]账号没有调用删除用户接口,返回403 Forbidden
@Test public void noHasDeleteUserAuthority() { MultiValueMap
form = new LinkedMultiValueMap<>(); form.set("username", "[email protected]"); form.set("password", "123456"); login(form); ResponseEntity entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE, new HttpEntity (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
entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE, new HttpEntity (loginHeaders), String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test public void deleteByRememberMeCookie() throws IOException { HttpHeaders loginHeaders = getHttpHeaders(0); ResponseEntity entity = this.restTemplate.exchange("http://localhost:8081/api/user/1", HttpMethod.DELETE, new HttpEntity (loginHeaders), String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); }
四.总结
- 本文主要介绍Spring Security使用数据库存储角色和权限,并在此情况下进行登录操作,同时介绍了记住我操作
上一篇:Spring Security 对Rest风格API的保护