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 项目,引入相关依赖
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
org.apache.commons
commons-lang3
mysql-connector-java
mysql
runtime
com.gitee.lin-mt
result-spring-boot-starter
自定义用户信息
/**
* 用户信息.
*
* @author lin-mt
*/
@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 的方法的话(如果我不懒的话,后续会有博客分析是在哪里调用的,也可以自己点源码看下)。
@Override
public void eraseCredentials() {
}
- 根据登录人输入的条件获取用户信息.
这就是 CURD 中的 Read 了(一丝丝熟悉的味道扑面而来),这部分需要我们自己实现,Spring Security 提供了几种实现,包括从内存中读取的 InMemoryUserDetailsManager,从数据库读取数据的 JdbcUserDetailsManager,当然我们也可以如下自定义实现接口 org.springframework.security.core.userdetails.UserDetailsService:
/**
* 用户 Service 实现类.
*
* @author lin-mt
*/
@Service
public 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 sysUserRoles = userRoleRepository.findByUserId(user.getId());
if (!CollectionUtils.isEmpty(sysUserRoles)) {
Set 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 lin-mt
*/
@Component
public 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 lin-mt
*/
@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 lin-mt
*/
@Configuration(proxyBeanMethods = false)
public class ApplicationConfig {
/**
* 注入密码加密方式.
*
* @return 密码加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
ApplicationController
/**
* ApplicationController.
*
* @author lin-mt
*/
@RestController
public class ApplicationController {
/**
*
* 需要拥有 ROLE_admin 权限才能访问的接口,此处设置权限时,如果是hasAnyRole,不需要以 ROLE_ 开头,在验证是否有权限的时候, 如果没有前缀,会自动加上前缀然后进行验证.
*
*
* @return Result
*/
@GetMapping("/admin")
@PreAuthorize("hasAnyRole('admin')")
public Result
测试
登录测试
在 SysUserServiceImpl 可以自己实现伪代码,初始化用户名以及用户拥有的权限,同时在ApplicationController 中添加相关的接口以测试。
①:我们自定义的处理 json 登录方式的地址
②:用户名和密码
③:自定义登录成功的返回数据
权限测试
惯例公众号: