写在前面
在前一篇我们学习了如何基于数据库来实现授权操作,使用了JdbcUserDetailsManager
类,同时发现它底层使用的是JdbcTemplate
,用户操作非常不方便,且使用了JdbcUserDetailsManager
自带的数据库,这些表和字段是无法满足实际的开发需要,因此通常做法是开发者自定义授权数据库和表。
出于操作简单的考虑,这里使用Spring Data JPA来代替JdbcTemplate
进而完成对数据库的操作。
创建工程
使用IDEA创建一个名为security-jpa的SpringBoot工程:
当然也可以在创建项目的时候不添加任何依赖,而是在后续pom.xml依赖文件中添加如下依赖:
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
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
|
之后开发者需要在MySQL数据库中新建一个名为securityjpa的数据库,这样就完成了创建工程和数据库的工作。
创建实体类
本篇主要介绍基于角色的权限控制,不涉及到具体的权限,因此只需角色Role和用户User实体类,但是在数据库中却需要用户角色表t_role、用户信息表t_user以及用户角色信息表t_user_role,请注意这个用户角色信息表t_user_role是可以自动生成的,因此用户只需创建角色Role和用户User这两个实体类。
新建entity包,并在其中创建用户角色类如下所示:
1 2 3 4 5 6 7 8 9 10 11
| @Data @Entity(name = "t_role") public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name;
private String description; }
|
Role实体类用户描述用户角色信息,它有角色id、名称和描述等属性,其中@Entity
注解表示这是一个实体类,当项目启动后,系统会根据实体类的属性在数据库中自动创建对应的角色表。
接着创建用户信息类如下所示:
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 63 64 65 66 67 68
| @Data @Entity(name = "t_user") public class User implements UserDetails {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
// @Getter(value = AccessLevel.NONE) private String username;
// @Getter(value = AccessLevel.NONE) private String password;
// @Getter(value = AccessLevel.NONE) private boolean accountNonExpired;
// @Getter(value = AccessLevel.NONE) private boolean accountNonLocked;
// @Getter(value = AccessLevel.NONE) private boolean credentialsNonExpired;
// @Getter(value = AccessLevel.NONE) private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST) private List<Role> roles;
@Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for(Role role:roles){ authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; }
@Override public String getPassword() { return password; }
@Override public String getUsername() { return username; }
@Override public boolean isAccountNonExpired() { return accountNonExpired; }
@Override public boolean isAccountNonLocked() { return accountNonLocked; }
@Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; }
@Override public boolean isEnabled() { return enabled; } }
|
请注意这里的用户信息类User需要实现UserDetails接口,并实现其中的所有方法。原因在于SpringSecurity存在多种认证方式,查看一下这个AuthenticationManagerBuilder
类,可以发现它存在inMemoryAuthentication
(内存)、jdbcAuthentication
(数据库)和ldapAuthentication
(LDAP)等三种认证方式:
1 2 3 4 5 6 7 8 9 10 11
| public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication() throws Exception { return (InMemoryUserDetailsManagerConfigurer)this.apply(new InMemoryUserDetailsManagerConfigurer()); }
public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication() throws Exception { return (JdbcUserDetailsManagerConfigurer)this.apply(new JdbcUserDetailsManagerConfigurer()); }
public LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> ldapAuthentication() throws Exception { return (LdapAuthenticationProviderConfigurer)this.apply(new LdapAuthenticationProviderConfigurer()); }
|
而且这些不同来源的数据认证都被被封装为一个UserDetailsService接口,任何实现了该接口的对象都可以作为认证的数据源,如下所示:
1 2 3
| public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
|
这个接口中只有一个loadUserByUsername方法,它通过用户名来加载用户信息,里面传入的是一个UserDetails对象,因此我们要想自定义使用SpringSecurity中的认证方式,那么必须自定义一个UserDetails对象,这也就是我们自定义的User实体类需要实现UserDetails接口的原因。
回到User类中,仔细看一下这个类所定义的属性,username、password、accountNonExpired、accountNonLocked、credentialsNonExpired和enabled分别表示用户名、密码、账号是否过期、账号是否锁住、密码是否过期和账号是否可用。可以发现这些属性都是从实现UserDetails接口中的方法名称中反推出来的,目的就是为了满足后续使用。
roles属性表示用户所具有的角色,请注意User和Role是多对多关系,因此需要使用@ManyToMany
注解来描述。还有就是实现UserDetails接口时重写的getAuthorities方法,这个方法用于返回用户的角色信息,这里我们选用了GrantedAuthority的一个名为SimpleGrantedAuthority的实现类,然后将用户角色的名称作为角色信息进行返回。
创建Repository
新建一个repository包,并在其中创建一个UserRepository接口,它需要继承JpaRepository接口,里面的代码如下所示:
1 2 3
| public interface UserRepository extends JpaRepository<User,Long> { User findUserByUsername(String username); }
|
上面是Spring Data JPA最基本的操作。
自定义UserDetailService
前面说了这些不同来源的数据认证都被被封装为一个UserDetailsService
接口,任何实现了该接口的对象都可以作为认证的数据源,而这里我们就将自定义的MyUserDetailService
作为数据源,不再使用之前的基于inMemoryAuthentication
和jdbcAuthentication
的认证。
新建一个service包,并在里面新建一个MyUserDetailService类,这个类需要实现UserDetailsService接口,并重写其中的loadUserByUsername
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Service public class MyUserDetailService implements UserDetailsService { @Autowired private UserRepository userRepository;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findUserByUsername(username); if(user==null){ throw new UsernameNotFoundException("用户不存在"); } return user; } }
|
在这个loadUserByUsername
方法中,参数是用户登录时输入的用户名,之后系统会根据用户名去数据库中查询用户信息,如果存在则自动进行密码匹配,不存在则直接抛出异常。
创建Controller
新建一个controller包,并在其中创建一个HelloController类,其中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello,envy!"; }
@GetMapping("/admin/hello") public String admin() { return "hello,admin!"; }
@GetMapping("/user/hello") public String user() { return "hello,user!"; } }
|
可以看到这个就是定义的需要测试的接口,和之前完全一样。
创建SecurityConfig
新建一个config包,并在其中创建一个MySecurityConfig类这个类需要继承WebSecurityConfigurerAdapter类,其中的代码如下所示:
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| @Configuration public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private MyUserDetailService myUserDetailService;
@Bean PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService); }
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**"); }
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .loginProcessingUrl("/goLogin") .usernameParameter("name") .passwordParameter("word") .successHandler((httpServletRequest, httpServletResponse, authentication) ->{ Object principal = authentication.getPrincipal(); httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write(new ObjectMapper().writeValueAsString(principal)); out.flush(); out.close(); } ) .failureHandler((httpServletRequest, httpServletResponse, e)-> { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); httpServletResponse.setStatus(401); Map<String, Object> map = new HashMap<>(); map.put("status", 401); if (e instanceof LockedException) { map.put("msg", "账户被锁定,登录失败!"); } else if (e instanceof BadCredentialsException) { map.put("msg", "账户名或密码输入错误,登录失败!"); } else if (e instanceof DisabledException) { map.put("msg", "账户被禁用,登录失败!"); } else if (e instanceof AccountExpiredException) { map.put("msg", "账户已过期,登录失败!"); } else { map.put("msg", "登录失败!"); } ObjectMapper objectMapper = new ObjectMapper(); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); }) .permitAll() .and() .csrf().disable() .exceptionHandling() .authenticationEntryPoint((httpServletRequest,httpServletResponse,authException)->{ httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("您尚未登录,请先登录!"); out.flush(); out.close(); }) .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler((httpServletRequest,httpServletResponse,authentication)->{ httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write("注销登录成功!"); out.flush(); out.close(); }).and(); }
//角色继承 @Bean RoleHierarchy roleHierarchy(){ RoleHierarchyImpl hierarchy= new RoleHierarchyImpl(); hierarchy.setHierarchy("ROLE_admin > ROLE_user"); return hierarchy; } }
|
可以看到这里几乎复用了前面的代码,但是注意以下新增的内容:
1 2 3 4 5 6 7
| @Autowired private MyUserDetailService myUserDetailService;
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService); }
|
可以看到这里依旧重写了configure(AuthenticationManagerBuilder auth)
方法,所不同的是此处没有使用之前的inMemoryAuthentication
和jdbcAuthentication
,而是使用了自定义的MyUserDetailService
作为数据源。
数据库和JPA配置
修改application.yml配置文件,在里面新增如下配置信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| spring: datasource: driver-class-name: com.mysql.jdbc.Driver username: root password: root url: jdbc:mysql:///securityjpa?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jpa: database: mysql database-platform: mysql hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect
|
启动项目测试
首先启动security-jpa项目的入口类SecurityJpaApplication,可以发现此时securityjpa数据库中就已经出现了3个表:t_user、t_role和t_user_role。
接着在SecurityJpaApplicationTests测试类中新增如下代码,用于往数据库中新增2个用户及角色信息:
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
| @SpringBootTest class SecurityJpaApplicationTests { @Autowired private UserRepository userRepository;
@Test void contextLoads() { User user1 = new User(); user1.setUsername("envy"); user1.setPassword("1234"); user1.setAccountNonExpired(true); user1.setAccountNonLocked(true); user1.setCredentialsNonExpired(true); user1.setEnabled(true);
List<Role> roles1 = new ArrayList<>(); Role r1 = new Role(); r1.setName("ROLE_admin"); r1.setDescription("管理员");
roles1.add(r1); user1.setRoles(roles1); userRepository.save(user1);
User user2 = new User(); user2.setUsername("test"); user2.setPassword("1234"); user2.setAccountNonExpired(true); user2.setAccountNonLocked(true); user2.setCredentialsNonExpired(true); user2.setEnabled(true);
List<Role> roles2 = new ArrayList<>(); Role r2 = new Role(); r2.setName("ROLE_user"); r2.setDescription("普通用户");
roles2.add(r2); user2.setRoles(roles2); userRepository.save(user2); } }
|
之后查看一下数据库,可以看到数据库中已经存在3个表及对应数据:
接口测试
将下来进行接口测试,首先以envy用户身份登录,之后依次访问/hello
,/admin/hello
和/user/hello
这三个接口,可以发现这三个接口都是可以正常访问的。
之后以test用户身份登录,可以发现它无法访问/admin/hello
接口,其他两个都是可以的。
需要说明的是,如果开发者将数据库中用户enabled字段由1(正常)修改为0(禁用),那么该用户就无法登陆系统,即登录失败:
enabled默认值为true,但是在数据库中则是以数字1进行存储,这是比较通用的做法。
这样本篇关于Spring Security中使用Spring Data JPA进行数据管理的学习就到此为止,后续学习其他内容。