Simple Demo
该系列都是基于前后端拆散的形式,返回的数据都是应用的 JSON,以及应用了自定义的返回后果 starter:https://gitee.com/lin-mt/result-spring-boot。
源码地址:https://gitee.com/lin-mt/spring-boot-examples/tree/master/spring-security-data-permission-control
新建一个 SpringBoot 我的项目,引入相干依赖
<dependency> <artifactId>spring-boot-starter-data-jpa</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <artifactId>spring-boot-starter-security</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <artifactId>spring-boot-starter-web</artifactId> <groupId>org.springframework.boot</groupId></dependency><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId></dependency><dependency> <artifactId>mysql-connector-java</artifactId> <groupId>mysql</groupId> <scope>runtime</scope></dependency><dependency> <groupId>com.gitee.lin-mt</groupId> <artifactId>result-spring-boot-starter</artifactId></dependency>
自定义用户信息
/** * 用户信息. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@Entity@Table(name = "sys_user")public class SysUser extends BaseEntity implements UserDetails, CredentialsContainer { private String username; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String secretCode; private int accountNonExpired; private int accountNonLocked; private int credentialsNonExpired; private int enabled; @Transient private Collection<? extends GrantedAuthority> authorities; // setter and getter @Basic @Override @Column(name = "username") public String getUsername() { return username; } @Override @Transient @JsonIgnore public String getPassword() { return getSecretCode(); } @Override @Transient public boolean isAccountNonExpired() { return 0 == this.accountNonExpired; } @Override @Transient public boolean isAccountNonLocked() { return 0 == this.accountNonLocked; } @Override @Transient public boolean isCredentialsNonExpired() { return 0 == this.credentialsNonExpired; } @Override @Transient public boolean isEnabled() { return 1 == this.enabled; } @Override public void eraseCredentials() { this.secretCode = null; } @Override public String toString() { return "SysUser{" + "username='" + username + '\'' + ", gender='" + gender + '\'' + ", phoneNumber='" + phoneNumber + '\'' + ", emailAddress='" + emailAddress + '\'' + ", accountNonExpired=" + accountNonExpired + ", accountNonLocked=" + accountNonLocked + ", credentialsNonExpired=" + credentialsNonExpired + ", enabled=" + enabled + ", authorities=" + authorities + '}'; }}
- 为什么要实现接口 org.springframework.security.core.userdetails.UserDetails 呢?
首先,Spring Security 必定须要依据用户输出的某个条件(通常是用户名,也就是 username )获取该条件对应的用户信息,而后再依据登录人输出的信息以及对应的用户信息去验证是否可能登录零碎。那么 Spring Security 怎么能力从用户信息中获取验证所须要的数据呢,用户信息是咱们返回给 Spring Security 的,无论是从内存还是数据库获取,都是包装成一个实体。重点来了,如果这个实体实现了某个接口,那么就能够将该实体向上转型为该接口的实体(这是 Java 根底哈),这时候就能够间接调用实体中接口的办法获取实体的数据!而后就能够依据这些数据验证登录人能不能进入零碎了,所以 UserDetails 接口中咱们要实现的几个办法中,返回的数据就是 Spring Security 用来验证的数据。
- 为什么实现接口 org.springframework.security.core.CredentialsContainer 呢?
网上很多的博客都没有实现该接口,这点被忽略了。在咱们胜利登陆之后,Spring Security 须要把咱们的登陆信息存储起来,这样咱们下次访问的时候才不须要反复校验,在第 1 点中有提到,返回的用户信息能够是咱们自定义的 ,那么为了避免数据透露(咱们可能须要把存储的用户信息返回给前端),存储的时候须要暗藏一些敏感信息,而 Spring Security 又不分明你自定义的信息中有哪些字段须要暗藏,那么就提供一个接口,在存储信息的时候调用该接口暗藏信息的办法,暗藏哪些信息取决于咱们在上面这个办法中做什么,Spring Security 会在缓存用户信息的时候调用该办法,如果你实现了 CredentialsContainer 的办法的话(如果我不懒的话,后续会有博客剖析是在哪里调用的,也能够本人点源码看下)。
@Overridepublic void eraseCredentials() {}
- 依据登录人输出的条件获取用户信息.
这就是 CURD 中的 Read 了(一丝丝相熟的滋味扑面而来),这部分须要咱们本人实现,Spring Security 提供了几种实现,包含从内存中读取的 InMemoryUserDetailsManager,从数据库读取数据的 JdbcUserDetailsManager,当然咱们也能够如下自定义实现接口 org.springframework.security.core.userdetails.UserDetailsService:
/** * 用户 Service 实现类. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@Servicepublic class SysUserServiceImpl implements SysUserService { private final SysUserRepository userRepository; private final SysUserRoleRepository userRoleRepository; private final SysRoleRepository roleRepository; private final PasswordEncoder passwordEncoder; public SysUserServiceImpl(SysUserRepository userRepository, SysUserRoleRepository userRoleRepository, SysRoleRepository roleRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.userRoleRepository = userRoleRepository; this.roleRepository = roleRepository; this.passwordEncoder = passwordEncoder; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user = userRepository.getByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } List<SysUserRole> sysUserRoles = userRoleRepository.findByUserId(user.getId()); if (!CollectionUtils.isEmpty(sysUserRoles)) { Set<Long> roleIds = sysUserRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet()); user.setAuthorities(roleRepository.findAllById(roleIds)); } return user; } }
注入的时候,@Service 最好是加上别名,因为 Spring Security 在 UserDetailsServiceAutoConfiguration 中默认条件注入了一个 InMemoryUserDetailsManager,所以,如果不取别名,在应用的时候就不晓得是应用的哪个 UserDetialsService 的实现了。
自定义从申请中获取登录信息的形式
Spring Security 默认是从 form 表单中获取 username 和 password,然而咱们应用的是 json 形式提交数据的,所以从 request 中获取登录信息就须要咱们自定义实现:
/** * 解决应用 Json 格局数据的登陆形式. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@Componentpublic class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Autowired public JsonAuthenticationFilter(ResultAuthenticationSuccessHandler authenticationSuccessHandler, ResultAuthenticationFailureHandler authenticationFailureHandler, @Lazy AuthenticationManager authenticationManager) { // 自定义该形式解决登录信息的登录地址,默认是 /login POST this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/loginByJson", "POST")); setAuthenticationSuccessHandler(authenticationSuccessHandler); setAuthenticationFailureHandler(authenticationFailureHandler); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException { if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) { final ObjectMapper mapper = new ObjectMapper(); UsernamePasswordAuthenticationToken authToken = null; try (final InputStream inputStream = request.getInputStream()) { final SysUser user = mapper.readValue(inputStream, SysUser.class); authToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); } catch (final IOException e) { authToken = new UsernamePasswordAuthenticationToken("", ""); throw new AuthenticationServiceException("Failed to read data from request", e.getCause()); } finally { setDetails(request, authToken); } // 进行登录信息的验证 return this.getAuthenticationManager().authenticate(authToken); } else { return super.attemptAuthentication(request, response); } }}
配置
SpringSecurityConfig
在该配置中,咱们自定义实现了登陆胜利后的返回数据、登录失败后的返回数据、权限认证失败后的返回数据,同时设置 Spring Security 读取用户信息的形式(咱们上一步自定义的 UserServiceDetails 实现)。
/** * Spring Security 配置. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@EnableWebSecurity@Configuration(proxyBeanMethods = false)@EnableGlobalMethodSecurity(prePostEnabled = true)public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { private final SysUserService userService; private final JsonAuthenticationFilter jsonAuthenticationFilter; private final ResultAccessDeniedHandler accessDeniedHandler; public SpringSecurityConfig(SysUserService userService, JsonAuthenticationFilter jsonAuthenticationFilter, ResultAccessDeniedHandler accessDeniedHandler) { this.userService = userService; this.jsonAuthenticationFilter = jsonAuthenticationFilter; this.accessDeniedHandler = accessDeniedHandler; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http.csrf().disable() .formLogin() .loginPage("/login") .permitAll() .and() .authorizeRequests() .mvcMatchers("/user/register").permitAll() .anyRequest().authenticated() .and() .exceptionHandling() .accessDeniedHandler(accessDeniedHandler); // @formatter:on http.addFilterAt(jsonAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); }}
ApplicationConfig
配置明码加密形式,咱们配置的明码加密形式,要跟咱们注册用户时的加密明码的形式一样,Spring Security 提供以下多种明码加密的形式,咱们就抉择其中的 DelegatingPasswordEncoder:
/** * 利用配置. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@Configuration(proxyBeanMethods = false)public class ApplicationConfig { /** * 注入明码加密形式. * * @return 明码加密形式 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}
ApplicationController
/** * ApplicationController. * * @author <a href="mailto:lin-mt@outlook.com">lin-mt</a> */@RestControllerpublic class ApplicationController { /** * <p> * 须要领有 ROLE_admin 权限能力拜访的接口,此处设置权限时,如果是hasAnyRole,不须要以 ROLE_ 结尾,在验证是否有权限的时候, 如果没有前缀,会主动加上前缀而后进行验证. * </p> * * @return Result */ @GetMapping("/admin") @PreAuthorize("hasAnyRole('admin')") public Result<Object> admin() { return Result.success().setMessage("This is admin index."); } /** * <p>须要领有 ROLE_user 权限能力拜访的接口.</p> * * @return Result */ @GetMapping("/user") @PreAuthorize("hasAnyRole('user')") public Result<Object> user() { return Result.success().setMessage("This is user index."); } }
测试
登录测试
在 SysUserServiceImpl 能够本人实现伪代码,初始化用户名以及用户领有的权限,同时在ApplicationController 中增加相干的接口以测试。
①:咱们自定义的解决 json 登录形式的地址
②:用户名和明码
③:自定义登录胜利的返回数据
权限测试
常规公众号: