2.Spring Security 用户,角色和权限 详解

一.前言

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. 数据库表设计

  • 用户,角色,权限,之间多对多关系2.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);
        }
    
  • 账号[email protected]账号没有调用删除用户接口,返回403 Forbidden
     @Test
    public void noHasDeleteUserAuthority() {
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.set("username", "[email protected]");
        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-metrue,此时返回值会生成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的保护

你可能感兴趣的:(2.Spring Security 用户,角色和权限 详解)