【框架学习】SMPE后端框架 - Spring Security

140 阅读30分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

目录

Spring Security

1. SecurityContextHolder 工具类

提供一些静态方法,目的是用来保存应用程序中当前使用人的安全上下文(调用接口的权限,每个用户的权限不同)。


// 获取安全上下文对象,就是那个保存在 ThreadLocal 里面的安全上下文对象
// 总是不为null(如果不存在,则创建一个authentication属性为null的empty安全上下文对象)
SecurityContext securityContext = SecurityContextHolder.getContext();

// 获取当前认证了的 principal(当事人),或者 request token (令牌)
// 如果没有认证,会是 null,该例子是认证之后的情况
Authentication authentication = securityContext.getAuthentication()

// 获取当事人信息对象,返回结果是 Object 类型,但实际上可以是应用程序自定义的带有更多应用相关信息的某个类型。
// 很多情况下,该对象是 Spring Security 核心接口 UserDetails 的一个实现类,你可以把 UserDetails 想像成我们数据库中保存的一个用户信息到 SecurityContextHolder 中 Spring Security 需要的用户信息格式的一个适配器。
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
	String username = ((UserDetails)principal).getUsername();
} else {
	String username = principal.toString();
}

2. Spring Security中将使用username和password封装成Authentication的实现类UsernamePasswordAuthenticationToken,声明为了authenticationToken

UsernamePasswordAuthenticationToken authenticationToken =
    new UsernamePasswordAuthenticationToken(userId, password);

Authentication authentication = null;
try {
    authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
} catch (BadCredentialsException e) {
    //账号或密码错误
    throw new BadRequestException(ResultEnum.LOGIN_FAIL);
} catch (InternalAuthenticationServiceException e) {
    throw new BadRequestException(ResultEnum.COUNT_NOT_ENABLE);
}
SecurityContextHolder.getContext().setAuthentication(authentication);

2.1 整个过程的讲解

Spring Security 是通过**AbstractAuthenticationProcessingFilter类向Web应用的基于HTTP、浏览器的请求提供身份验证服务。**

2.1.1 UsernamePasswordAuthenticationFilter类的说明

UsernamePasswordAuthenticationFilter类是AbstractAuthenticationProcessingFilter抽象类针对 使用 用户名和密码 进行身份验证 而定制化的一个过滤器类

首先我们来了解一下AbstractAuthenticationProcessingFilter抽象类在框架中的角色和职责

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ih4uGAwK-1638009468755)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\AbstractAuthenticationProcessingFilter.png)]

AbstractAuthenticationProcessingFilter抽象类在整个身份验证的流程中主要处理的工作就是所有与Web资源相关的事情,并且将其封装成Authentication对象,最后调用AuthenticationManager的验证方法。

所以UsernamePasswordAuthenticationFilter类的工作大致也是这样。只不过当前情境下更加明确了Authentication对象的封装数据的来源–用户名和密码。

AbstractAuthenticationProcessingFilter抽象类的方法和属性如图所示。

UsernamePasswordAuthenticationFilter类继承拓展了AbstractAuthenticationProcessingFilter抽象类,有了一下几个改动:

  1. 属性中增加了usernameParameter、passwordParameter和postOnly属性。
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
  1. 此类的构造器中强制指定请求的方式为POST。
public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
}
  1. 重写了attemptAuthentication身份验证入口方法(创建了UsernamePasswordAuthenticationToken类对象来封装request中传过来的username和password)
public Authentication attemptAuthentication(HttpServletRequest request,
                                            HttpServletResponse response) throws AuthenticationException {
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
    }

    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) {
        username = "";
    }

    if (password == null) {
        password = "";
    }

    username = username.trim();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);

    // 将我们当前请求request中的session,ip等放到token中
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
}

2.1.2 封装用户名和密码的基石:UsernamePasswordAuthenticationToken类

UsernamePasswordAuthenticationToken类需要从HttpRequest中获取username和password字段,并将其封装进Authentication中传递给AuthenticationManager进行身份验证。

Authentication接口中属性如图

UsernamePasswordAuthenticationFilter类的功能是 用户名和密码的过滤器 。给它一组用户名和密码,如果匹配,那么就算验证成功,否则验证失败。

此类的attemptAuthentication身份验证入口方法 定义了UsernamePasswordAuthenticationToken类对象来存储从request中获取到的username 和 password。看上方的代码块。

UsernamePasswordAuthenticationToken类继承了AbstractAuthenticationToken抽象类,与抽象类的主要区别为 将username赋值给属性principal,将password赋值给credentials。

public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
                                           Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true); // must use super, as we override
}

UsernamePasswordAuthenticationFilter类attemptAuthentication方法中,通过UsernamePasswordAuthenticationToken实例化了Authentication接口,继而按照流程,将其传递给AuthenticationMananger调用身份验证核心完成相关工作。

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
    username, password);

//将我们当前请求request中的session,ip等放到token中
    setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
2.1.2.1 第一部分

上方程序的第一部分,创建了一个还没有经过认证的token,principal和credentials属性分别为username 和 password

//初始化为 未认证过的token,通过认证后,会重新调用另一个UsernamePasswordAuthenticationToken构造方法,将authenticated属性设置为true
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
    //父类权限,当前为登录,所有权限为空
    super(null);
    this.principal = principal;
    this.credentials = credentials;
    //设置是否认证属性为false
    setAuthenticated(false);
}

super(null)详解

UsernamePasswordAuthenticationToken类的父类的构造器为自定义构造器,子类构造器中需要显示调用父类构造器 。父类构造器需要authorities参数,表示权限集合。当前场景为登录,所以权限集合设置为空。

public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {

    private final Collection<GrantedAuthority> authorities;
    private Object details;
    private boolean authenticated = false;

	//构造器 需要给authorities属性赋值
    public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
        //如果传入的参数为null,authorities设置为常量
        /**
        public abstract class AuthorityUtils {
            public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();
		}
        */
        if (authorities == null) {
            this.authorities = AuthorityUtils.NO_AUTHORITIES;
            return;
        }

        for (GrantedAuthority a : authorities) {
            if (a == null) {
                throw new IllegalArgumentException(
                    "Authorities collection cannot contain any null elements");
            }
        }
        ArrayList<GrantedAuthority> temp = new ArrayList<>(
            authorities.size());
        temp.addAll(authorities);
        //将temp集合变成一个不能修改的集合,给authorities属性赋值
        this.authorities = Collections.unmodifiableList(temp);
    }

Collection authorities属性详解

GrantedAuthority是一个接口,里面定义getAuthority方法

public interface GrantedAuthority extends Serializable {
	String getAuthority();
}

通常使用GrantedAuthority接口的一个实现类SimpleGrantedAuthority

定义一个String类型属性role 用来存放用户的权限

重写接口的getAuthority方法,返回对象的role属性

public final class SimpleGrantedAuthority implements GrantedAuthority {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	
	private final String role;

	public SimpleGrantedAuthority(String role) {
		Assert.hasText(role, "A granted authority textual representation is required");
		this.role = role;
	}
	
	@Override
	public String getAuthority() {
		return role;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}

		if (obj instanceof SimpleGrantedAuthority) {
			return role.equals(((SimpleGrantedAuthority) obj).role);
		}

		return false;
	}

	@Override
	public int hashCode() {
		return this.role.hashCode();
	}

	@Override
	public String toString() {
		return this.role;
	}
}
2.1.2.2 第二部分

上方程序的第二部分 setDetails(request,authRequest); 调用UsernamePasswordAuthenticationFilter类中的setDetails方法

protected void setDetails(HttpServletRequest request,UsernamePasswordAuthenticationToken authRequest) {
   //将request作为authRequest的details属性的值(把我们当前请求request中的session、ip等放到token中)
  authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

UsernamePasswordAuthenticationToken类继承于AbstractAuthenticationToken类,类中定义了三个属性

public abstract class AbstractAuthenticationToken implements Authentication,CredentialsContainer {
    // ~ Instance fields
    // ================================================================================================
	//权限集合
    private final Collection<GrantedAuthority> authorities;
    //详情
    private Object details;
    //是否通过验证
    private boolean authenticated = false;
}
2.1.2.3 第三部分

return this.getAuthenticationManager().authenticate(authRequest); 返回认证之后的Authentication接口类型的实现类。详见2.1.3

2.1.3 验证核心的工作者:AuthenticationProvider

AuthenticationManager接口在设计上并不是用于完成特定的身份验证工作,而是调用其配发的AuthenticationProvider接口去实现的。

到这里就有一个疑问:

针对接口声明参数 声明的Authentication,针对不同验证协议的AuthenticationProvider的实现类们是如何完成对应的工作的,并且AuthenticationManager是如何知道应该使用哪一个AuthenticationProvider才能完成对应协议的验证工作?

AuthenticationProvider只包含两个方法声明,核心验证方法入口:

Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

另一个是让AuthenticationManager可以 通过调用此方法 辨别当前AuthenticationProvider 是否能完成响应验证工作 的supports方法。

boolean supports(Class<?> authentication);

简单来说:AuthenticationProvider有两个方法,authenticate方法用来验证当前的Authentication接口的实现类

supports方法用来返回 AuthenticationProvider接口能不能验证当前的Authentication。

在Spring Security中唯一AuthenticationManager的实现类ProviderManager,在处理authenticate身份验证入口方法时,首先解决的问题便是:哪一个AuthenticationProvider实现类可以验证当前传入的Authentication?

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    for (AuthenticationProvider provider : getProviders()) {
    //...
}
}

UsernamePasswordAuthenticationFilter类attemptAuthentication方法中

return this.getAuthenticationManager().authenticate(authRequest);

this.getAuthenticationManager() 调用了从抽象类AbstractAuthenticationProcessingFilter中继承的方法,返回AuthenticationManager接口的对象。实际上返回的是实现了AuthenticationManager接口的类的对象。AuthenticationManager接口的实现类为ProviderManager。

也就是执行的是providerManager.authenticate(authRequest);

UsernamePasswordAuthenticationFilter类和ProviderManager类中的程序执行 如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9VkTm4gu-1638009468761)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\providerManager程序执行.png)]

在ProviderManager的视角里,所有的Authentication接口的实现类都不具名,ProviderManager不仅不能通过自身完成 验证工作,也不能独立完成辨别当前AuthenticationProvider 是否能完成响应验证工作。

而是统统交给AuthenticationProvider去完成。而不同的AuthenticationProvider接口的实现类开发的初衷就是为了支持指定的某种验证协议,所以在特定的AuthenticaitonProvider的视角中,它只关心当前Authentication是不是它预先设计处理的类型即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xc4cNUzL-1638009468762)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\AuthenticationProvider接口的方法.png)]

DaoAuthenticationProvider类是AbstractUserDetailsAuthenticationProvider抽象类的子类。AbstractUserDetailsAuthenticationProvider抽象类实现了AuthenticationProvider接口。

AbstractUserDetailsAuthenticationProvider抽象类中实现了AuthenticationProvider接口的supports方法

DaoAuthenticationProvider针对UsernamePasswordAuthenticationToken的大部分逻辑都是通过AbstractUserDetailsAuthenticationProvider完成的。比如针对ProviderManager询问是否支持当前Authentication的supports方法:

//AuthenticationProvider接口的supports方法
boolean supports(Class<?> authentication);
//AbstractUserDetailsAuthenticationProvider实现后的supports方法
public boolean supports(Class<?> authentication) {
    return (UsernamePasswordAuthenticationToken.class
            .isAssignableFrom(authentication));
}

isAssignableFrom方法是判断两个类之间是否存在继承关系。

DaoAuthenticationProvider类会判断当前的Authentication的实现类是否是UsernamePasswordAuthenticationToken它本身,或者是扩展了UsernamePasswordAuthenticationToken的子孙类。返回true的场景只有一种,便是当前的Authentication是UsernamePasswordAuthenticationToken实现。

核心验证逻辑:AbstractUserDetailsAuthenticationProvider抽象中实现的authenticate方法

public Authentication authenticate(Authentication authentication)
    throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                        () -> messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.onlySupports",
                            "Only UsernamePasswordAuthenticationToken is supported"));

    // Determine username
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
        : authentication.getName();

    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    if (user == null) {
        cacheWasUsed = false;

        try {
            user = retrieveUser(username,
                                (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
            logger.debug("User '" + username + "' not found");

            if (hideUserNotFoundExceptions) {
                throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
            }
            else {
                throw notFound;
            }
        }

        Assert.notNull(user,
                       "retrieveUser returned null - a violation of the interface contract");
    }

    try {
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,
                                       (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
        if (cacheWasUsed) {
            // There was a problem, so try again after checking
            // we're using latest data (i.e. not from the cache)
            cacheWasUsed = false;
            user = retrieveUser(username,
                                (UsernamePasswordAuthenticationToken) authentication);
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                                           (UsernamePasswordAuthenticationToken) authentication);
        }
        else {
            throw exception;
        }
    }

    postAuthenticationChecks.check(user);

    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;

    if (forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }

    return createSuccessAuthentication(principalToReturn, authentication, user);
}

当ProviderManager.authenticate方法执行了以下代码后,就会将验证工作交给DaoAuthenticationProvider进行处理

if (!provider.supports(toTest)) {
    continue;
}

if (debug) {
    logger.debug("Authentication attempt using "
                 + provider.getClass().getName());
}
//此处开始调用DaoAuthenticationProvider类中的authenticate方法。
try {
    result = provider.authenticate(authentication);

    if (result != null) {
        copyDetails(authentication, result);
        break;
    }
}

与ProviderManager最不同的一点是,在DaoAuthenticationProvider的视角中,当前的Authentication最起码一定是UsernamePasswordAuthenticationToken的形式了,不用和ProviderManager一样因为匮乏信息而不知道干什么。然后DaoAuthenticationProvider分别会按照预先设计的一样分别从authentication的principal和credentials中获取用户名和密码。

String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
 
String presentedPassword = authentication.getCredentials().toString();

2.2 总结

  1. UsernamePasswordAuthenticationFilter扩展AbstractAuthenticationProcessingFilter,因为需要从HTTP请求中从指定名称的参数获取用户名和密码,并且传递给验证核心;
  2. UsernamePasswordAuthenticationToken扩展Authentication,因为我们设计了一套约定将用户名和密码放入了指定的属性中以便核心读取使用;
  3. DaoAuthenticationProvider 扩展AuthenticationProvider,因为我们需要在核心中对UsernamePasswordAuthenticationToken进行处理,并按照约定读出用户名和密码使其可以进行身份验证操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9omrTulR-1638009468763)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\总结.png)]

2.3 详解AbstractUserDetailsAuthenticationProvider类中的authenticate方法

  1. 获取authentication接口实现类中的username属性
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
        : authentication.getName();

AbstractUserDetailsAuthenticationProvider抽象类定义属性时,使用了多态,实现类对象指向接口的变量

private UserCache userCache = new NullUserCache();
//定义 缓存是否使用的局部变量 cacheWasUserd = true
boolean cacheWasUsed = true;
// NullUserCache类中 getUserFromCache方法
//public UserDetails getUserFromCache(String username) {
//		return null;
//	}
UserDetails user = this.userCache.getUserFromCache(username);
  1. 如果user为空,调用retrieveUser方法
if (user == null) {
    cacheWasUsed = false;
	
    try {
        user = retrieveUser(username,
                            (UsernamePasswordAuthenticationToken) authentication);
    }
    //...
}

AbstractUserDetailsAuthenticationProvider抽象的retrieveUser方法没有方法体

protected abstract UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
  1. 实际调用的是实现类DaoAuthenticationProvider中的实现的retrieveUser方法
protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
    prepareTimingAttackProtection();
    try {
        UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException(
                "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }
    //...
}

retrieveUser方法中的重要语句 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

DaoAuthenticationProvider类中定义了UserDetailsService接口的属性 userDetailsService;

private UserDetailsService userDetailsService;

UserDetailsService接口 里面定义了loadUserByUsername方法

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  1. 拿到属性后调用属性的loadUserByUsername方法 实际调用的是 我们自定义的userDetailService接口的实现类中重写的loadUserByUsername方法
@RequiredArgsConstructor
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    private final IUserService userService;

    private final IRoleService roleService;

    private final IDataService dataService;

    private final LoginProperties loginProperties;

    private final UserMapStruct userMapStruct;

    private final RedisUtils redisUtils;

    public void setEnableCache(boolean enableCache) {
        this.loginProperties.setCacheEnable(enableCache);
    }

    /**
     * description:
     * modify @RenShiWei 2020/11/21 description:修改为根据id生成token,改变身份认证策略,根据id查询
     *
     * @param username 查询参数 实质为user_id
     * @return /
     * @author RenShiWei
     * Date: 2020/11/21 21:24
     */
    @Override
    public JwtUserDto loadUserByUsername(String username) {
        boolean searchDb = true;
        JwtUserDto jwtUserDto = null;

        if (loginProperties.isCacheEnable() && redisUtils.hasKey(username)) {
            jwtUserDto = (JwtUserDto) SerializationUtils.deserialize((byte[]) redisUtils.get(username));
            searchDb = false;
        }
        if (searchDb) {
            UserDTO userDto;
            UserBO user = userService.findUserDetailById(Long.parseLong(username));
            userDto = userMapStruct.toDto(user);
            if (user.getIsAdmin()) {
                userDto.setIsAdmin(true);
            }
            if (ObjectUtil.isEmpty(userDto)) {
                throw new BadRequestException(ResultEnum.USER_NOT_EXIST);
            } else {
                if (! userDto.getEnabled()) {
                    throw new BadRequestException(ResultEnum.COUNT_NOT_ENABLE);
                }
                jwtUserDto = new JwtUserDto(
                        userDto,
                        dataService.getDataScopeWithDeptIds(userDto),
                        roleService.mapToGrantedAuthorities(userDto)
                );
                redisUtils.set(username, SerializationUtils.serialize(jwtUserDto));
            }
        }
        return jwtUserDto;
    }
}

2.3.1 详解loadUserByUsername方法

2.3.1.1 语句一

初始设置两个局部变量 searchDb(是否查询数据库) jwtUserDto(存放登录用户的信息,数据权限,权限)

boolean searchDb = true;
JwtUserDto jwtUserDto = null;

JwtUserDto类

UserDetails接口的实现类,定义了三个属性:代表当前登录用户信息的UserDTO类的user,当前登录用户数据权限集合dataScopes,当前登录用户的权限集合authorities。

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class JwtUserDto implements UserDetails, Serializable {


    private UserDTO user;

    private List<Long> dataScopes;

    @JsonIgnore
    private List<GrantedAuthority> authorities;

    /**
     * description:获取角色权限信息
     *
     * @return 权限信息
     * @author RenShiWei
     * Date: 2020/11/18 12:11
     */
    public Set<String> getRoles() {
        return authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
    }

    public void setRoles(Set<String> roles) {
        authorities =
                roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }

    @Override
    @JsonIgnore
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    @JsonIgnore
    public String getUsername() {
        return user.getUsername();
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return user.getEnabled();
    }
}

先解释UserDTO类 此类继承BaseBTO类

@EqualsAndHashCode(callSuper = true)
@Data
public class UserDTO extends BaseDTO implements Serializable {
	
    private static final long serialVersionUID = 3061801174078023207L;
	//用户id
    private Long id;
	//用户角色集合(例如 超级管理员 普通用户)
    private Set<RoleSmallDTO> roles;
	//用户职位集合(例如 人事 产品)
    private Set<JobSmallDTO> jobs;
	//用户所属部门(例如 研发部)
    private DeptSmallDTO dept;
	//部门id
    private Long deptId;
	//用户名
    private String username;
	//用户昵称
    private String nickName;
	//邮箱
    private String email;
	//手机号
    private String phone;
	//性别
    private Boolean gender;
	//头像名(数据库sys_user表中没有此字段)
    private String avatarName;
	//头像路径(数据库sys_user表中有此字段,但都没有使用)
    private String avatarPath;
	//前端加密后的密码
    @JsonIgnore
    private String password;
	//用户状态(是否可用)对应数据库sys_user表中的is_deleted字段
    private Boolean enabled;
	//是否为超级管理员 默认为false
    @JsonIgnore
    private Boolean isAdmin = false;
	//密码修改日期
    private LocalDateTime pwdResetTime;
}

BaseDTO类

@Getter
@Setter
@ToString
public class BaseDTO implements Serializable {

    private static final long serialVersionUID = - 4289279747443003946L;

    /** 创建者 */
    private Long createBy;

    /** 更新者 */
    private Long updatedBy;

    /** 创建时间 */
    private LocalDateTime createTime;

    /** 更新时间 */
    private LocalDateTime updateTime;

}
2.3.1.2 语句二

如果 isCacheEnable()返回true 且 redis中有键为username 则jwtUserDto局部变量 使用redis中存储的value,且searchDb = false(searchDb的值代表是否查询数据库)

if (loginProperties.isCacheEnable() && redisUtils.hasKey(username)) {
    jwtUserDto = (JwtUserDto) SerializationUtils.deserialize((byte[]) redisUtils.get(username));
    searchDb = false;
}

LoginProperties类用来读取yml配置文件中的login配置。类中定义属性cacheEnable以及isCacheEnable方法

@Data
@ConfigurationProperties(prefix = "login")
@Configuration
public class LoginProperties {
    private boolean cacheEnable;
    public boolean isCacheEnable() {
        return cacheEnable;
    }
}

对应配置文件中的

# 登录相关配置
login:
  # 登录缓存
  cache-enable: true

如果用户是首次登录,或者之前登录过,但是redis中的存储的登录信息已经过期,那么if语句的真值为false,需要从数据库中查询用户信息。否则为true,从redis中取出用户的信息。

2.3.1.3 语句三

查询各个表,将查询结果放入JwtUserDto类型的对象中,并放入redis缓存中。

if (searchDb) {
    UserDTO userDto;
    UserBO user = userService.findUserDetailById(Long.parseLong(username));
    userDto = userMapStruct.toDto(user);
    if (user.getIsAdmin()) {
        userDto.setIsAdmin(true);
    }
    if (ObjectUtil.isEmpty(userDto)) {
        throw new BadRequestException(ResultEnum.USER_NOT_EXIST);
    } else {
        if (! userDto.getEnabled()) {
            throw new BadRequestException(ResultEnum.COUNT_NOT_ENABLE);
        }
        jwtUserDto = new JwtUserDto(
            userDto,
            dataService.getDataScopeWithDeptIds(userDto),
            roleService.mapToGrantedAuthorities(userDto)
        );
        redisUtils.set(username, SerializationUtils.serialize(jwtUserDto));
    }
}
return jwtUserDto;
2.3.1.3.1 部分一
UserDTO userDto;
UserBO user = userService.findUserDetailById(Long.parseLong(username));

定义一个UserDTO类型的局部变量userDto,根据传入的userId查询用户的信息,定义UserBO类型的对象user接收

UserBO类 继承User类

@EqualsAndHashCode(callSuper = true)
@Data
public class UserBO extends User {

    private static final long serialVersionUID = 5209955359940119094L;
	//用户所在部门
    private Dept dept;
	//用户的角色(可以有多个)
    private Set<Role> roles;
	//用户的职位(可以有多个)
    private Set<Job> jobs;

}

User类

@Data
@Accessors(chain = true)
@ToString(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@ApiModel(value = "User对象", description = "系统用户")
@TableName("sys_user")
public class User extends BasicModel<User> {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "部门部门id")
    private Long deptId;

    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "昵称")
    private String nickName;

    @ApiModelProperty(value = "性别")
    private Boolean gender;

    @ApiModelProperty(value = "手机号码")
    private String phone;

    @ApiModelProperty(value = "邮箱")
    private String email;

    @ApiModelProperty(value = "头像路径")
    private String avatarPath;

    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "是否为admin账号")
    private Boolean isAdmin;

    @ApiModelProperty(value = "状态:1启用、0禁用")
    private Boolean enabled;

    @ApiModelProperty(value = "修改密码的时间")
    private LocalDateTime pwdResetTime;

    @Override
    protected Serializable pkVal() {
        return this.id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        User user = (User) o;
        return Objects.equals(id, user.id) &&
                Objects.equals(username, user.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, username);
    }

}

UserBO user = userService.findUserDetailById(Long.parseLong(username));

找到IUserService接口的实现类UserServiceImpl类,查看重写的findUserDetailById方法

@Override
@Transactional(rollbackFor = Exception.class)
public UserBO findUserDetailById(Long id) {
    UserBO userBO = userMapper.findUserDetailById(id);
    if (ObjectUtil.isNull(userBO)) {
        throw new BadRequestException(ResultEnum.DATA_NOT_FOUND);
    }
    return userBO;
}

查看UserMapper映射文件中的sql语句

/**
     * description 通过用户名查询该用户信息,包括所在部分,拥有的job,和角色(角色中又包含menu)
     * 该方法只在用户第一次登陆时执行,之后将结果信息存入缓存
     *
     * @param id 用户id
     * @return User
     * @author Wangmingcan
     * Date : 2020-08-23 15:50
     */
@Select("SELECT id,dept_id,username,nick_name,gender,phone,email,avatar_path,password," +
        "is_admin,enabled,create_by,update_by,pwd_reset_time,create_time,update_time" +
        " FROM sys_user u WHERE u.id = #{id} AND is_deleted=0")
@Results({
    @Result(column = "id", property = "id"),
    @Result(column = "dept_id", property = "deptId"),
})
@Queries({
    @Query(column = "id", property = "roles",
           select = "marchsoft.modules.system.mapper.RoleMapper.findWithMenuByUserId"),
    @Query(column = "id", property = "jobs",
           select = "marchsoft.modules.system.mapper.JobMapper.findByUserId"),
    @Query(column = "dept_id", property = "dept",
           select = "marchsoft.modules.system.mapper.DeptMapper.selectById")
})
@Cacheable(key = "'id:' + #p0")
UserBO findUserDetailById(Long id);

查询sys_user表中的用户记录 查询条件为传入的id = 表中记录的id字段 且 is_deleted = 0 (是否逻辑删除等于0 表示用户状态为可用) 且关联查询roles(用户权限集合) jobs(用户职位之和)dept(用户所属部门)查询结果封装到UserBO类中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGLXj82N-1638009468764)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\sys_user表.png)]

自定义注解@Query 将查询A表得到的字段x作为查询B表的条件,实现联表查询

/**
 * 重新实现 @One@Many注解
 * 关联查询注解,用于mapper层查询方法上
 * 仅需关联查询一个属性时,可以直接使用@Query
 * 如果需要使用多个@Query,请先使用@Queries套在外层(类似 @Results)
 * 参数、作用基本和原@One保持一致,既可以用于一对一也可以一对多
 *
 * @author Wangmingcan
 * Date: 2021/01/12 09:35
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
//指定用来保存@Query注解内容的容器为@Queries @Queries注解中放多个@Query
@Repeatable(Queries.class)
public @interface Query {

    /** 被关联的列,一般为id,请保证和实体类中属性名称一致,驼峰下划线都可以
     * 如关联的列为dept_id,填deptId和dept_id都可以*/
    String column() default "";

    /** 关联的属性 ,和实体类中需要封装的属性名称保持一致*/
    String property() default "";

    /** 执行的查询方法,建议填写mapper层的全限定方法名 ,方法返回值必须和property类型一致*/
    String select() default "";

}

解释@Query注解

@Query(column = "id", property = "roles",
       select = "marchsoft.modules.system.mapper.RoleMapper.findWithMenuByUserId"),
@Query(column = "id", property = "jobs",
       select = "marchsoft.modules.system.mapper.JobMapper.findByUserId"),
@Query(column = "dept_id", property = "dept",
       select = "marchsoft.modules.system.mapper.DeptMapper.selectById")

UserBO findUserDetailById(Long id)方法对应的sql语句查询到了用户的id和dept_id等字段。使用这两个字段进行联表查询

查询条件为**sys_user表中的id 等于 sys_users_roles表中的user_id** 关联的mapper映射文件中的方法为RoleMapper.findWithnMenuByUserId

/**
     * description 通过用户id查询角色(包含角色的菜单信息)
     *
     * @param userId 用户id
     * @return 角色(包含角色的菜单信息)
     * @author Wangmingcan
     * @date 2020-08-23 15:49
     */
@Select("SELECT r.id,r.name,r.level,r.description,r.data_scope,r.is_protection,r.create_by,r.update_by,r" +
        ".create_time,r" +
        ".update_time " +
        "FROM sys_role r, sys_users_roles ur WHERE r.id = ur.role_id AND ur.user_id = ${userId}" +
        " AND r.is_deleted=0")
//数据库每条记录的字段名 和 程序中定义的类的属性 的映射关系是在
// @Results中value属性的@Result注解进行配置的
//数据库sys_role表中每条记录的is_protection对应RoleBO类的protection属性  id字段对应RoleBO类中的id属性
@Results({
    @Result(column = "id", property = "id"),
    @Result(column = "is_protection", property = "protection")
})
@Query(column = "id", property = "menus",
       select = "marchsoft.modules.system.mapper.MenuMapper.findByRoleId")
//看笔记
@Cacheable(key = "'user:' + #p0")
//解释此方法中使用的关联查询
/**
     * 方法需求为: 通过用户id查询角色(包含角色的菜单信息)
     * 此方法传入的参数为userId 通过查询sys_users_roles表 (别名 ur)和 sys_role表 (别名r)
     * 传入的userId 与 ur中的user_id相等 在此基础上 ur中的role_id = r 中的 id
     * 可以查到指定userId的用户的角色
     *
     * 此时还需要查询用户的权限(menus) 使用@Query 来实现关联查询
     *  @Query(column = "id", property = "menus",
     *             select = "marchsoft.modules.system.mapper.MenuMapper.findByRoleId")
     * 将userId 对应的role_id 作为参数 传给 MenuMapper 中的 findByRoleId 方法
     * Set<Menu> findByRoleId(Long id);
     */
Set<RoleBO> findWithMenuByUserId(Long userId);

sys_users_roles表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eFPmEYuV-1638009468766)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\sys_users_roles表.png)]

sys_role表

Set findWithMenuByUserId(Long userId) 方法映射的sql语句 查询sys_role表中的信息 返回结果为Set类型对象 UserBO类中有Set类型的属性roles,RoleBO类型继承于Role类型。关联查询中 Set findWithMenuByUserId(Long userId) 方法 返回的结果Set类型对象,传递给UserBO类型的Set类型的属性roles 此处为向上转型。RoleBO类型中独有的menus属性和depts属性,UserBO获取不到。

查询条件为 sys_user表中的id(关联查询传入) 等于 sys_users_roles表中的 user_id ,满足此条件的记录的job_id 等于 sys_role表中的id。且sys_role表的is_deleted字段为0(角色状态为可用) Set findWithMenuByUserId(Long userId)方法对应的sql语句查询到了sys_role表中的id字段。使用这个字段进行联表查询关联的mapper映射文件中的方法为MenuMapper.findByRoleId

/**
     * description 通过角色id和关联表roles_menus查询该角色拥有的菜单
     *
     * @param id 菜单id
     * @return Set<Menu>
     * @author Jiaoqianjin
     * @date 2020-11-23 15:45
     */
@Select("SELECT m.id,m.pid,m.sub_count,m.type,m.title,m.name,m.component,m.menu_sort,m.icon,m.path," +
        "m.i_frame,m.cache,m.hidden,m.permission,m.create_by,m.update_by,m.create_time,m.update_time " +
        "FROM sys_menu m, sys_roles_menus rm " +
        "WHERE m.id = rm.menu_id AND rm.role_id = ${id} AND m.is_deleted=0")
@Cacheable(key = "'role:' + #p0")
Set<Menu> findByRoleId(Long id);

sys_roles_menus表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fP1YAMdp-1638009468769)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\sys_roles_menus表.png)]

sys_menu表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YEpLN2iS-1638009468770)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\sys_menu表.png)]

Set

2.3.1.3.2 部分二
userDto = userMapStruct.toDto(user);

将UserBO类型的局部变量user 转型为 UserDTO类型 ,用userDto接收

实际上调用userMapStruct接口的实现类UserMapStructImpl中重写的toDto方法

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-08-25T14:30:20+0800",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_152 (Oracle Corporation)"
)
@Component
public class UserMapStructImpl implements UserMapStruct {

    @Autowired
    private RoleMapStruct roleMapStruct;
    @Autowired
    private DeptMapStruct deptMapStruct;
    @Autowired
    private JobMapStruct jobMapStruct;
    
    //...
    
    //将UserBO类型转为UserDTO类型
    @Override
    public UserDTO toDto(UserBO entity) {
        if ( entity == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();

        userDTO.setCreateBy( entity.getCreateBy() );
        userDTO.setCreateTime( entity.getCreateTime() );
        userDTO.setUpdateTime( entity.getUpdateTime() );
        userDTO.setId( entity.getId() );
        //UserBO类中的Set<Role>类型的属性roles 此处要将Role类型转换成RoleSmallDTO类型(Role类型的属性对应sys_role表的所有字段,RoleSmallDTO类型中只有id,name,level,dataScope属性)
        userDTO.setRoles( roleSetToRoleSmallDTOSet( entity.getRoles() ) );
        //UserBO类中的Set<Job>类型的属性jobs 此处要将Job类型转换成JobSmallDTO类型 (Job类型的属性对应sys_job表的所有字段,JobSmallDTO类型中只有id,name属性)
        userDTO.setJobs( jobSetToJobSmallDTOSet( entity.getJobs() ) );
        //UserBO类中的Dept类型的属性dept 此处要将Dept类型转换成DeptSmallDTO类型 (Dept类型的属性对应sys_dept表的所有子字段,DeptSmallDTO类型中只有id,name属性)
        userDTO.setDept( deptToDeptSmallDTO( entity.getDept() ) );
        userDTO.setDeptId( entity.getDeptId() );
        userDTO.setUsername( entity.getUsername() );
        userDTO.setNickName( entity.getNickName() );
        userDTO.setEmail( entity.getEmail() );
        userDTO.setPhone( entity.getPhone() );
        userDTO.setGender( entity.getGender() );
        userDTO.setAvatarPath( entity.getAvatarPath() );
        userDTO.setPassword( entity.getPassword() );
        userDTO.setEnabled( entity.getEnabled() );
        userDTO.setIsAdmin( entity.getIsAdmin() );
        userDTO.setPwdResetTime( entity.getPwdResetTime() );

        return userDTO;
    }
    //...
    protected Set<RoleSmallDTO> roleSetToRoleSmallDTOSet(Set<Role> set) {
        if ( set == null ) {
            return null;
        }

        Set<RoleSmallDTO> set1 = new HashSet<RoleSmallDTO>( Math.max( (int) ( set.size() / .75f ) + 1, 16 ) );
        for ( Role role : set ) {
            set1.add( roleToRoleSmallDTO( role ) );
        }

        return set1;
    }
    
    protected Set<JobSmallDTO> jobSetToJobSmallDTOSet(Set<Job> set) {
        if ( set == null ) {
            return null;
        }

        Set<JobSmallDTO> set1 = new HashSet<JobSmallDTO>( Math.max( (int) ( set.size() / .75f ) + 1, 16 ) );
        for ( Job job : set ) {
            set1.add( jobToJobSmallDTO( job ) );
        }

        return set1;
    }
    
    protected DeptSmallDTO deptToDeptSmallDTO(Dept dept) {
        if ( dept == null ) {
            return null;
        }

        DeptSmallDTO deptSmallDTO = new DeptSmallDTO();

        deptSmallDTO.setId( dept.getId() );
        deptSmallDTO.setName( dept.getName() );

        return deptSmallDTO;
    }
}
2.3.1.3.3 部分三
if (user.getIsAdmin()) {
    userDto.setIsAdmin(true);
}

user.getIsAdmin()方法返回UserBO类型变量user的isAdmin属性 如果为true,那么UserDTO类型变量userDto的isAdmin属性也为true。isAdmin属性对应sys_user表中的is_admin字段。默认为false。我们定义了一个name为admin的用户,并设置is_admin字段为true。此用户作为本系统设置的最高权限的管理员。当方法中查看当前用户的isAdmin属性为true时,会让此用户执行单独的方法。

2.3.1.3.4 部分四
if (ObjectUtil.isEmpty(userDto)) {
    throw new BadRequestException(ResultEnum.USER_NOT_EXIST);
}
else {
    if (! userDto.getEnabled()) {
        throw new BadRequestException(ResultEnum.COUNT_NOT_ENABLE);
    }
    jwtUserDto = new JwtUserDto(
        userDto,
        dataService.getDataScopeWithDeptIds(userDto),
        roleService.mapToGrantedAuthorities(userDto)
    );
    //将userId作为键 序列化之后的jwtUserDto对象作为值 存入redis中
    redisUtils.set(username, SerializationUtils.serialize(jwtUserDto));
}

如果userDto为空,抛异常

如果userDto的enabled属性为false,抛异常 enabled属性代表用户状态(true代表可用,false代表禁用)

dataService.getDataScopeWithDeptIds(userDto) 实际执行的为IDataService接口的实现类DataServiceImpl中重写的方法

@Override
@Cacheable(key = "'user:' + #p0.id")
public List<Long> getDataScopeWithDeptIds(UserDTO user) {
    // 用于存储部门id
    Set<Long> deptIds = new HashSet<>();
    // 查询用户角色
    List<RoleSmallDTO> roleSet = roleService.findRoleByUserId(user.getId());
    // 获取对应的部门ID
    for (RoleSmallDTO role : roleSet) {
        DataScopeEnum dataScopeEnum = DataScopeEnum.find(role.getDataScope());
        switch (Objects.requireNonNull(dataScopeEnum)) {
            case THIS_LEVEL:
                //如果是本级设置为当前用户的部门id
                // MODIFY description: 现在本级权限包括子部门,如果以后是多部门需要修改本方法 @liuxingxing 2021-02-06
                ArrayList<Dept> depts = new ArrayList<Dept>() {{
                    add(deptService.getById(user.getDeptId()));
                }};
                deptIds.addAll(deptService.getDeptChildren(depts));
                break;
            case CUSTOMIZE:
                //当前角色的部门和子部门权限
                deptIds.addAll(getCustomize(deptIds, role));
                break;
            default:
                //默认为全部,返回null
                return new ArrayList<>(deptIds);
        }
    }
    return new ArrayList<>(deptIds);
}

查询用户角色

List<RoleSmallDTO> roleSet = roleService.findRoleByUserId(user.getId());

调用IRoleService接口的实现类RoleServiceImpl中重写的findRoleByUserId方法

@Override
public List<RoleSmallDTO> findRoleByUserId(Long userId) {
    //根据userId查询用户具有的角色
    Set<Role> roles = roleMapper.findRoleByUserId(userId);
    if (CollectionUtil.isEmpty(roles)) {
        throw new BadRequestException(ResultEnum.DATA_NOT_FOUND);
    }
    //将Set<Role> 类型转换为 List<RoleSmallDTO> 类型
    return roleSmallMapStruct.toDto(new ArrayList<>(roles));
}

获取角色对应的部门id

for (RoleSmallDTO role : roleSet) {
    DataScopeEnum dataScopeEnum = DataScopeEnum.find(role.getDataScope());
    switch (Objects.requireNonNull(dataScopeEnum)) {
        case THIS_LEVEL:
            //如果是本级设置为当前用户的部门id
            // MODIFY description: 现在本级权限包括子部门,如果以后是多部门需要修改本方法 @liuxingxing 2021-02-06
            ArrayList<Dept> depts = new ArrayList<Dept>() {{
                add(deptService.getById(user.getDeptId()));
            }};
            deptIds.addAll(deptService.getDeptChildren(depts));
            break;
        case CUSTOMIZE:
            //当前角色的部门和子部门权限
            deptIds.addAll(getCustomize(deptIds, role));
            break;
        default:
            //默认为全部,返回null
            return new ArrayList<>(deptIds);
    }
}
return new ArrayList<>(deptIds);

sys_role表中的data_scope字段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BHYoGc7p-1638009468771)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\sys_role表.png)]

分别是 “全部”,"自定义“。

在DataScopeEnum 数据权限枚举类型中

@Getter
@AllArgsConstructor
public enum DataScopeEnum {

    /* 全部的数据权限 */
    ALL("全部", "全部的数据权限"),

    /* 自己部门的数据权限 */
    THIS_LEVEL("本级", "自己部门的数据权限"),

    /* 自定义的数据权限 */
    CUSTOMIZE("自定义", "自定义的数据权限");

    private final String value;
    private final String description;
	//形参val 为role.getDataScope() 返回的数据权限
    public static DataScopeEnum find(String val) {
        //DataScopeEnum.values() 返回一个数组,数组元素为枚举类中定义的对象常量 将数组作为循环体
        for (DataScopeEnum dataScopeEnum : DataScopeEnum.values()) {
            //如果val 等于 枚举中定义的对象常量的value属性 返回此对象常量
            if (val.equals(dataScopeEnum.getValue())) {
                return dataScopeEnum;
            }
        }
        return null;
    }

}

增强for循环例子

//增强for循环 语法 for(数据类型 变量名:数组或集合对象){循环体}
/**
        例子
        List<String> strs = Arrays.asList("aa","bb","cc");
        for(String str:strs){
        	System.out.pringtln(str);
        }
        结果
        aa
        bb
        cc
*/

使用switch case分支语句 根据对应的dataScopeEnum做出不同的处理

case THIS_LEVEL:
//获取用户所在的部门id 加入部门id集合中
ArrayList<Dept> depts = new ArrayList<Dept>() {{
    add(deptService.getById(user.getDeptId()));
}};
//deptIds 为Set集合 不能存放重复元素 getDeptChildren方法(参数为用户所在的部门id集合) 返回一个List集合 里面存放用户所在部门 和 子部门的 Id
deptIds.addAll(deptService.getDeptChildren(depts));
break;
/* 自己部门的数据权限 */
THIS_LEVEL("本级", "自己部门的数据权限"),

例如 当前用户部门为 华南分部 华南分部有两个子部门:研发部和运维部

那么 当**用户的角色的数据权限对应数据权限枚举类对象为THIS_LEVEL时**,deptIds中应当是本级部门的id 和 子部门的id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKHLuZvS-1638009468771)(G:\三月\Java文件\JAVA路线\Typora笔记\项目-框架\SMPE\SMPE框架分享图片\photo\部门之间的关系.png)]

详解getDeptChildren方法

@Override
//形参为用户所在部门的 部门id集合
public List<Long> getDeptChildren(List<Dept> deptList) {
    //新建一个List集合存放 用户所在部门 和 子部门的 Id
    List<Long> list = new ArrayList<>();
    deptList.forEach(dept -> {
        //如果当前元素不为空且当前部门状态为可用 将其加入集合
        if (ObjectUtil.isNotNull(dept) && dept.getEnabled()) {
            list.add(dept.getId());
            //根据用户当前部门id 查询子部门id 放入集合 sys_dept表中的pid字段表示当前部门的上级部门id
            List<Dept> depts = this.findByPid(dept.getId());
            //判断条件:用户所在部门是否有子部门
            if (depts.size() != 0) {
                //存在子部门 使用递归 调用getDeptChildren方法 
                list.addAll(getDeptChildren(depts));
            }
        }
    });
    return list;
}

findByPid方法

@Override
public List<Dept> findByPid(long pid) {
    //构造一个LambdaQueryWrapper对象,类型为Dept对象
    LambdaQueryWrapper<Dept> deptLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //Dept::getPid lambda表达式 调用Dept类中的getPid方法返回当前部门的上级部门id,与传入的部门id比较 如果相等的话 返回符合条件的数据库中的记录的集合
    deptLambdaQueryWrapper.eq(Dept::getPid, pid);
    /**
   	遍历sys_dept表,将pid 等于 形参的 记录放入list集合中返回
    default List<T> list(Wrapper<T> queryWrapper) {
        return getBaseMapper().selectList(queryWrapper);
    }
    */
    return list(deptLambdaQueryWrapper);
}

switch case 中的default 表示当前参数 不符合所有的case 则使用default中定义的方法 来处理参数

default:
//默认为全部,返回null
return new ArrayList<>(deptIds);

此处是指sys_role表中 data_scope字段为 ”全部“ 在DataScopeEnum枚举类中对应的对象常量为ALL。但在switch case 中没有ALL的处理方法。所以当dataScopeEnum属性为ALL时,用来存储部门id的Set类型的属性deptIds 为空集合。方便程序辨认此用户为权限最高的管理员。

获取用户所属角色对应的权限集合

roleService.mapToGrantedAuthorities(userDto)

调用IRoleService实现类RoleServiceImpl重写的mapToGrantedAuthorities方法

/**
     * description 查询用户的角色权限信息
     *
     * @param user 用户信息
     * @return 用户的角色权限信息
     * @author Wangmingcan
     * @date 2020-08-23 16:06
*/
@Override
@Cacheable(key = "'auth:' + #p0.id")
public List<GrantedAuthority> mapToGrantedAuthorities(UserDTO user) {
    //Set<String>类型的集合中存放用户的权限信息
    Set<String> permissions = new HashSet<>();
    // 如果是管理员直接返回
    if (user.getIsAdmin()) {
        permissions.add("admin");
        //遍历permissions,将元素作为SimpleGrantedAuthority类的构造器参数
        //public SimpleGrantedAuthority(String role) {
        //      Assert.hasText(role, "A granted authority textual representation is required");
        //      this.role = role;
        //}
        //最后将所有SimpleGrantedAuthority类对象放入一个list集合中
        return permissions.stream().map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());
    }
    //根据用户id查询角色、权限信息 使用RoleBO类型对象封装
    //	public class RoleBO extends Role {
    //		@ApiModelProperty("角色对应的菜单集合")
    //		private Set<Menu> menus;
    //		@ApiModelProperty("角色对应的部门集合")
    //		private Set<Dept> depts;
	//	}
    Set<RoleBO> roles = roleMapper.findWithMenuByUserId(user.getId());
    //
    permissions = roles.stream()
        .flatMap(role -> role.getMenus().stream())
        //过滤掉Set<Menu>类型的属性menus 中 permission属性为空的元素 sys_menu 中permission 字段存放 描述权限的字符串(例如user:add,roles:del)
        .filter(menu -> StringUtils.isNotBlank(menu.getPermission()))
        //获取每个Menu类型元素的permission属性 并放入set<String> permissions中
        .map(Menu::getPermission).collect(Collectors.toSet());
    //遍历permissions,将元素作为SimpleGrantedAuthority类的构造器参数
    return permissions.stream().map(SimpleGrantedAuthority::new)
        .collect(Collectors.toList());
}

2.3.2 详解check方法

AbstractUserDetailsAuthenticationProvider类中的authenticate方法

通过调用retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication)方法 获取到UserDetails类型的局部变量user后

try {
    preAuthenticationChecks.check(user);
    additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
    if (cacheWasUsed) {
        // There was a problem, so try again after checking
        // we're using latest data (i.e. not from the cache)
        cacheWasUsed = false;
        user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
        preAuthenticationChecks.check(user);
        additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
    }
    else {
        throw exception;
    }
}
2.3.2.1 第一部分
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
preAuthenticationChecks.check(user);

实际调用UserDetailsChecker接口的实现类DefaultPreAuthenticationChecks中实现的check方法

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
    public void check(UserDetails user) {
        //检查用户账号是否被锁定
        if (!user.isAccountNonLocked()) {
            //控制台打印异常信息,抛异常
            //...
        }
		//检查用户状态是否为可用
        if (!user.isEnabled()) {
            //控制打印异常信息,抛异常
            //...
        }
		//检查用户账号是否超时
        if (!user.isAccountNonExpired()) {
            //控制打印异常信息,抛异常
            //...
        }
    }
}
//返回用户账号是否被锁定
user.isAccountNonLocked()
//返回用户状态 true 可用 false 禁用
user.isEnabled()
//返回用户账号是否过期
user.isAccountNonExpired()
//返回用户认证是否过期
user.isCredentialsNonExpired()

JwtUserDto 实现了UserDetails接口 重写了上面的方法

@JsonIgnore
@Override
public boolean isAccountNonExpired() {
    return true;
}

@JsonIgnore
@Override
public boolean isAccountNonLocked() {
    return true;
}

@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
    return true;
}

@Override
@JsonIgnore
public boolean isEnabled() {
    return user.getEnabled();
}
2.3.2.2 第二部分
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);

实际执行的是AbstractUserDetailsAuthenticationProvider抽象类的实现类DaoAuthenticationProvider中实现的additionalAuthenticationChecks方法

protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
    //authentication的credentials属性 为用户的登录密码 为空抛异常
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");

        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));
    }
	//拿到authentication中credentials属性,转成String类型
    String presentedPassword = authentication.getCredentials().toString();
	//比较JwtUserDto类型的对象中的password 与 转成String类型的 authentication中credentials属性 是否一致 不一致抛异常
    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");

        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials",
            "Bad credentials"));
    }
}
2.3.2.3 第三部分
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
    this.userCache.putUserInCache(user);
}
//将 JwtUserDto类型 赋值给Object类型变量
Object principalToReturn = user;

if (forcePrincipalAsString) {
    principalToReturn = user.getUsername();
}

实际调用UserDetailsChecker接口的实现类DefaultPostAuthenticationChecks中实现的check方法

private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
    public void check(UserDetails user) {
        //返回用户认证是否过期
        if (!user.isCredentialsNonExpired()) {
            //控制台打印异常信息,抛异常
            //...
        }
    }
}
if (!cacheWasUsed) {
    this.userCache.putUserInCache(user);
}

在此前的代码中 cacheWasUsed的值为false

NullUserCache类中putUserInCache方法 方法体为空

public void putUserInCache(UserDetails user) {}
//AbstractUserDetailsAuthenticationProvider类定义的属性
private boolean forcePrincipalAsString = false;
//由于if条件为false 不执行principalToReturn = user.getUsername();
if (forcePrincipalAsString) {
    principalToReturn = user.getUsername();
}

2.3.3 详解createSuccessAuthentication方法

protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
    // Ensure we return the original credentials the user supplied,
    // so subsequent attempts are successful even with encoded passwords.
    // Also ensure we return the original getDetails(), so that future
    // authentication events after cache expiry contain the details
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
        principal, authentication.getCredentials(),
        authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());

    return result;
}

当认证过通过后,创建一个新的token。

参数一:Object principalToReturn = user; user类型为JwtUserDto

参数二:UsernamePasswordAuthenticationToken类型的authentication。principal属性为userId,credentials属性为password。 此处使用authentication的principal属性(userId)

参数三:JwtUserDto类型的user。

private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
authoritiesMapper.mapAuthorities(user.getAuthorities())

user.getAuthorities()返回用户的权限集合

NullAuthoritiesMapper类是GrantedAuthoritiesMapper接口的实现类

mapAuthorities方法

传入的参数 直接返回

public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
   return authorities;
}

所以参数三为 List类型的authorities

使用UsernamePasswordAuthenticationToken类中的三个参数的构造器 创建一个新的token

public UsernamePasswordAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = principal;
    this.credentials = credentials;
    super.setAuthenticated(true); // must use super, as we override
}

此时,UsernamePasswordAuthenticationToken类的authorities属性有了值

表示是否认证的 authenticated 属性设置为true(初始设置为false)

UsernamePasswordAuthenticationToken类中的三个参数的构造器中没有给Object类型的details属性赋值

result.setDetails(authentication.getDetails());

2.4 result = provider.authenticate(authentication)后的程序执行

for (AuthenticationProvider provider : getProviders()) {
    if (!provider.supports(toTest)) {
        continue;
    }

    if (debug) {
        logger.debug("Authentication attempt using "
                     + provider.getClass().getName());
    }

    try {
        result = provider.authenticate(authentication);

        if (result != null) {
            copyDetails(authentication, result);
            break;
        }
    }
    catch (AccountStatusException e) {
        prepareException(e, authentication);
        // SEC-546: Avoid polling additional providers if auth failure is due to
        // invalid account status
        throw e;
    }
    catch (InternalAuthenticationServiceException e) {
        prepareException(e, authentication);
        throw e;
    }
    catch (AuthenticationException e) {
        lastException = e;
    }
}
//private AuthenticationManager parent; ProviderManager类的authenticate方法没有给parent赋值 parent == null
//此判断条件为false 不执行
if (result == null && parent != null) {
    // Allow the parent to try.
    try {
        result = parent.authenticate(authentication);
    }
    catch (ProviderNotFoundException e) {
        // ignore as we will throw below if no other exception occurred prior to
        // calling parent and the parent
        // may throw ProviderNotFound even though a provider in the child already
        // handled the request
    }
    catch (AuthenticationException e) {
        lastException = e;
    }
}

//private boolean eraseCredentialsAfterAuthentication = true;
//此处result的类型为UsernamePasswordAuthenticationToken,它是CredentialsContainer接口的实现类
//((CredentialsContainer) result).eraseCredentials();
//去掉result中的credentials属性值 此时credentials存放的为用户的登录密码
//UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(userId, password);
if (result != null) {
    if (eraseCredentialsAfterAuthentication
        && (result instanceof CredentialsContainer)) {
        // Authentication is complete. Remove credentials and other secret data
        // from authentication
        ((CredentialsContainer) result).eraseCredentials();
    }
    //此处调用ProviderManager类中定义的AuthenticationEventPublisher接口的实现类NullEventPublisher的方法,方法体为空
	//private static final class NullEventPublisher implements AuthenticationEventPublisher {
	//	public void publishAuthenticationFailure(AuthenticationException exception,
	//			Authentication authentication) {
	//	}
	//
	//	public void publishAuthenticationSuccess(Authentication authentication) {
	//	}
	//}
    eventPublisher.publishAuthenticationSuccess(result);
    return result;
}

// Parent was null, or didn't authenticate (or throw an exception).

if (lastException == null) {
    lastException = new ProviderNotFoundException(messages.getMessage(
        "ProviderManager.providerNotFound",
        new Object[] { toTest.getName() },
        "No AuthenticationProvider found for {0}"));
}

prepareException(lastException, authentication);

throw lastException;

3. AuthorizationController类login方法

3.1 String token = tokenProvider.createToken(authentication, jwtUserDto.getUser().getId());

String token = tokenProvider.createToken(authentication, jwtUserDto.getUser().getId());

tokenProvider.createToken方法

public String createToken(Authentication authentication, Long userId) {
    //获取权限列表 authentication类中有List<GrantedAuthority> authorities属性 
    //GrantedAuthority接口的实现类SimpleGrantedAuthority中String类型的role属性 存放用户的权限
    //getAuthority方法 返回role属性
    String authorities = authentication.getAuthorities().stream()
        .map(GrantedAuthority::getAuthority)
        .collect(Collectors.joining(","));

    return jwtBuilder
        // 加入ID确保生成的 Token 都不一致
        .setId(IdUtil.simpleUUID())
        // 声明中加入权限信息,方便从token中取出
        .claim(AUTHORITIES_KEY, authorities)
        //设置生成token的主题(依据什么生成token)
        .setSubject(userId.toString())
        .compact();
}

使用JwtBuilder接口的实现类DefaultJwtBuilder的一些方法 来生成token

简单描述token组成部分:header(头部),payload/claims(载荷),signature(签名)

载荷就是存放有效信息的地方,这些有效信息包含三个部分

标准中注册的声明
公共的声明
私有的声明

标准中注册的声明 不需要强制设置

iss: 签发者
sub: 面向用户
aud: 接收者
iat(issued at): 签发时间
exp(expires): 过期时间
nbf(not before):不能被接收处理时间,在此之前不能被接收处理
jti:JWT ID为web token提供唯一标识

其中载荷部分存在两个属性 payload 和 claims 。两者均可作为载荷,JwtBuilder接口中,只能设置一个属性,同时设置报错

Claims是一个接口类型

public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> {
    String ISSUER = "iss";
    String SUBJECT = "sub";
    String AUDIENCE = "aud";
    String EXPIRATION = "exp";
    String NOT_BEFORE = "nbf";
    String ISSUED_AT = "iat";
    String ID = "jti";
    //getter setter方法
}

Claims接口的实现类DefaultClaims中 实现了七个键的setValue he getValue方法

setId方法

public JwtBuilder setId(String jti) {
    if (Strings.hasText(jti)) {
        this.ensureClaims().setId(jti);
    } else if (this.claims != null) {
        this.claims.setId(jti);
    }

    return this;
}
this.ensureClaims().setId(jti);

首先 this.ensureClaims() 方法 返回DefaultClaims类型对象

protected Claims ensureClaims() {
    if (this.claims == null) {
        this.claims = new DefaultClaims();
    }

    return this.claims;
}

然后调用DefaultClaims中setId方法

public Claims setId(String jti) {
    this.setValue("jti", jti);
    return this;
}

claim方法

public JwtBuilder claim(String name, Object value) {
    Assert.hasText(name, "Claim property name cannot be null or empty.");
    if (this.claims == null) {
        if (value != null) {
            this.ensureClaims().put(name, value);
        }
    } else if (value == null) {
        this.claims.remove(name);
    } else {
        this.claims.put(name, value);
    }

    return this;
}

DefaultClaims类实现Claims接口 Claims接口继承Map<String,Object>

name作为键,value作为值

setSubject方法

public JwtBuilder setSubject(String sub) {
    if (Strings.hasText(sub)) {
        this.ensureClaims().setSubject(sub);
    } else if (this.claims != null) {
        this.claims.setSubject(sub);
    }

    return this;
}

执行过程同setId

compact方法

里面对token的header和signature部分进行设置

3.2 onlineUserService.save(jwtUserDto, token, request);保存在线信息

更多相关文章如下

【Java全栈】Java全栈学习路线及项目全资料总结【JavaSE+Web基础+大前端进阶+SSM+微服务+Linux+JavaEE】
blog.csdn.net/qq_45696377…