Administrator
发布于 2026-05-03 / 1 阅读
0
0

spring-security认识及项目应用-202509120153

spring-security认识及项目应用-202509120153

spring-security认识及项目应用

spring-security认识及项目应用

官方说明:https://docs.spring.io/spring-security/reference/index.html

spring-security介绍

什么是spring-security

Spring Security 是一个为 Spring 应用提供认证和权限控制的安全框架,功能强大、可扩展、可配

置。

核心功能

Spring Security 与其他安全方案对比

功能

说明

认证(Authentication)

验证用户是谁(比如登录)

授权(Authorization)

判断用户是否有权限访问资源(比如访问某个

接口或操作)

基于角色的访问控制

控制不同角色用户的访问权限,如 ADMIN、

USER

密码加密与管理

支持多种加密算法,如 BCrypt,保障账号安

防护功能

防止常见攻击:CSRF、XSS、Session Fixatio

n、Clickjacking 等

集成第三方认证

支持 OAuth2、JWT、LDAP、CAS、SSO 等

方法级安全控制

通过注解如 @PreAuthorize 、@Secure

d 实现细粒度控制

URL 访问控制

可以配置哪些路径谁可以访问,如 /admin/

** 只能管理员访问

自动登录/记住我

提供“记住我”功能,在 Cookie 中保存登录状

集成spring-security

对比维度

Spring Security

Apache Shiro

手动拦截器/Filter

✅ 功能丰富度

功能非常全,支持认

证、授权、加密、防

攻击、OAuth2、JW

T 等

功能偏轻,适合权限

认证,但不支持 OAu

th2 等高级特性

需要自己实现所有功

能,灵活但容易出错

✅ 与 Spring 集成

原生支持,Spring Bo

ot 自动配置,生态丰

需要手动整合 Spring

自己维护拦截器链或

过滤器,整合 Spring

较繁琐

✅ 灵活性

高,自定义 Provide

r、过滤器链、表达式

权限、注解控制

中等,认证逻辑可自

定义,权限控制有限

非常高,但不规范,

权限控制常零散在各

✅ 安全性

默认安全性高(CSR

F、Session Fixatio

n、密码加密等开箱

即用)

需手动配置安全防护

容易遗漏关键安全点

(如 CSRF、XSS),

不利于合规

✅ 社区支持

Spring 官方支持,文

档丰富,社区活跃

社区相对小,维护活

跃度一般

无框架支持,社区支

持取决于你用的技术

❌ 学习成本

学习曲线较陡,配置

复杂,概念多(Filter

Chain、Provider、To

ken等)

学习门槛低,上手快

无学习成本,但长期

维护代价高

❌ 配置复杂度

DSL 配置语法灵活但

容易误用,注解、配

置项较多

配置直观,代码集中

自由配置,但容易混

乱、不统一

❌ 适用范围

中大型 Spring 项目、

微服务、需要 OAuth

2、JWT 的系统

小型、权限简单的系

快速开发、个人小项

目、临时性系统

如果引入spring-boot-starter-security 依赖,恭喜已经有了基本的登录功能了,其实还没

有如何的业务代码

但是在接口访问的的时候检查cookie,如果没有会转跳到默认的登录界面

要问为什么,只能说这是spring-securtiy的默认功能(不喜欢就配置自己的登录)

登录

登录校验

实现验证请求中用户信息

配置使用的验证方式

org.springframework.boot

spring-boot-starter-security

/**

*创建认证管理器,使用 DaoAuthenticationProvider 进行基于数据库的认证。

*/

@Bean

public AuthenticationManager

authenticationManager(UserDetailsService userDetailsService) {

DaoAuthenticationProvider provider = new

DaoAuthenticationProvider();

provider.setUserDetailsService(userDetailsService);

provider.setPasswordEncoder(passwordEncoder());

return new ProviderManager(provider);

}

/**

  • 创建认证提供者,使用 DaoAuthenticationProvider 进行基于数据库的认证。
  • 绑定用户详情服务,密码编码器
  • */

    @Bean

    public AuthenticationProvider authenticationProvider() {

    DaoAuthenticationProvider provider = new

    DaoAuthenticationProvider();

    provider.setUserDetailsService(userDetailsService());

    provider.setPasswordEncoder(passwordEncoder());

    return provider;

    }

    /**

    UserDetailsServiceImpl

    数据库查询数据用于上面的DaoAuthenticationProvider,提供的用户数据

    只要将用户数据查出来组装成UserDetails即可,具体的校验校验逻辑由spring-security实现(比如密

    码的校验)

    做完这些我们就得到了基本的用户登录校验,如果用户没有登录,那么就会被拦截

    只用请求中携带的信息与查询到的用户名密码匹配才能正确访问

    *注入自定义的用户详情服务实现类。

    */

    @Bean

    public UserDetailsService userDetailsService() {

    return new UserDetailsServiceImpl(); // 实现见下

    }

    /**

  • 创建密码编码器,使用 BCryptPasswordEncoder 进行密码
  • */

    @Bean

    public PasswordEncoder passwordEncoder() {

    return new BCryptPasswordEncoder();

    }

    @Service

    public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired

    private UserMapper userMapper;

    @Override

    public UserDetails loadUserByUsername(String username) throws

    UsernameNotFoundException {

    SysUserPo sysUser = userMapper.selectOne(new

    LambdaQueryWrapper().eq(SysUserPo::getUsername, username));

    if (sysUser == null) {

    throw new UsernameNotFoundException("用户不存在");

    }

    return User.builder()

    .username(sysUser.getUsername())

    .password(sysUser.getPassword())

    .disabled(false)

    .roles("USER") // 角色也可以从数据库读取

    .build();

    }

    }

    验证的基本流程

    如何获取登录信息

    jwt是无状态登录,服务器不存储登录信息

    sessin是有状态登录,服务器是存储sessin的,所以并不需要再次查询数据库验证

    Provider与ProviderManager

    在 Spring Security 中,AuthenticationProvider 是认证的核心接口之一,用于处理具体的认证逻

    辑。 DaoAuthenticationProvider 就是它的一个实现类,用户数据库查询验证

    ProviderManager 它负责管理多个 AuthenticationProvider,根据当前请求的 Authentication 类型

    选择合适的 provider 来完成认证。

    1.调用 AuthenticationManager.authenticate(authentication)

    2. 遍历所有 provider,找到支持当前 authentication 类型的 provider →

    DaoAuthenticationProvider

    3. DaoAuthenticationProvider.authenticate() 开始执行:

    a. 使用 UserDetailsService.loadUserByUsername(username) 加载用户详情

    b. 检查用户是否存在

    c. 使用 PasswordEncoder.matches(rawPassword, encodedPassword) 验证密码

    d. 如果都通过,构建已认证的 UsernamePasswordAuthenticationToken 并返回

    4. 认证成功,

    SecurityContextHolder.getContext().setAuthentication(authentication)

    5. 后续权限控制基于这个 authentication 对象

    请求到达

    经过 Security 过滤器链

    → 如果是 Session 模式:

  • 从 Cookie 中获取 JSESSIONID
  • 查找对应的 HttpSession
  • 从中恢复 SecurityContext 和 Authentication
  • 判断是否 isAuthenticated()
  • → 如果是 JWT 模式:

  • 用户请求
  • JwtAuthenticationFilter解析请求中的jwt
  • 调用 UserDetailsService 加载用户信息
  • 创建 Authentication 并设置到 SecurityContextHolder
  • 如何选用合适的provider?

    自定义的多个provider会有各自的supports() 方法,表示支持哪种Authentication,只有支持的

    才会走对应的provider

    而DaoAuthenticationProvider 默认支持的是UsernamePasswordAuthenticationT

    oken 

    seeeion登录

    根据响应头j-session设置cookie是浏览器的默认行为,当然后续携带也是浏览器的行为(前端无需

    操作)

    所以我们只要验证后设置session即可

    @Override

    public boolean supports(Class authentication) {

    return

    SmsAuthenticationToken.class.isAssignableFrom(authentication);

    }

    /**

    *前端再cookie中设置jsessionId,然后每次请求都带上sessionId

  • 前端无法直接删除cookie,修改调用后端接口使cookie失效
  • *

  • 如果已经通过了session校验,那么会自动跳过jwt校验(避免重复骄校验,spring-
  • security的功能)

    */

    @PostMapping("/login-session")

    public Response loginSession(@RequestBody LoginRequest

    loginRequest, HttpServletRequest request) {

    UsernamePasswordAuthenticationToken token =

    new

    UsernamePasswordAuthenticationToken(loginRequest.getUsername(),

    loginRequest.getPassword());

    try {

    // 调用 Spring Security 的认证机制(走

    DaoAuthenticationProvider)

    Authentication authentication =

    authenticationManager.authenticate(token);

    // ✅ 设置认证信息到 SecurityContext


    SecurityContextHolder.getContext().setAuthentication(authentication);

    jwt登录

    什么是jwt

    JWT 是一种 基于 JSON 格式 的令牌,具有以下特点:

    无状态:服务端不保存登录会话,全部信息都在 token 中。

    自包含:token 本身包含了用户身份、权限等信息。

    可验证性强:token 是用私钥签名的,服务端可以验证其完整性和真实性。

    一个典型的 JWT 看起来像这样(其实是base64字符):

    它由三部分组成(中间用 . 分隔):

    代码

    登录,输入用户名,密码,返回jwt

    // ✅ 将认证信息写入 HttpSession(注意:Spring Security 会自动使

    用 SESSION_ID 做关联)

    HttpSession session = request.getSession(true); // true 表示

    没有就创建

    session.setAttribute("SPRING_SECURITY_CONTEXT",

    SecurityContextHolder.getContext());

    return Response.success("登录成功,sessionId: " +

    session.getId());

    } catch (BadCredentialsException e) {

    return Response.error("用户名或密码错误");

    }

    }

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.

    SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

    部分

    作用

    内容示例

    Header

    说明使用的算法

    {"alg": "HS256", "ty

    p": "JWT"} 

    Payload

    存放数据(如用户ID、角色

    等)

    {"sub": "123456789

    0", "name": "john",

    "iat": 1516239022} 

    Signature

    签名(防篡改)

    对前两部分使用密钥签名

    返回jwt字符串即可,剩下的让前端来(前端在收到token后存储,后续每次请求携带token)

    生成与解析jwt的工具类

    @PostMapping("/login-jwt")

    public Response loginJwt(@RequestBody LoginRequest

    loginRequest) {

    UsernamePasswordAuthenticationToken token =

    new

    UsernamePasswordAuthenticationToken(loginRequest.getUsername(),

    loginRequest.getPassword());

    try {

    Authentication authentication =

    authenticationManager.authenticate(token);

    // 登录成功,生成 JWT

    String username = authentication.getName();

    Map claims = Map.of(

    "roles", authentication.getAuthorities().stream()

    .map(GrantedAuthority::getAuthority)

    .toList()

    );

    String jwtToken = JwtUtil.generateToken(username, claims);

    return Response.success(jwtToken);

    } catch (BadCredentialsException e) {

    return Response.error("用户名或密码错误");

    }

    }

    /**

  • jwt相关
  • *

  • @author wuyuxinag
  • @since 2025-07-09
  • */

    public class JwtUtil {

    // ✅ 推荐保存在配置中(application.yml),这里写死方便演示

    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor("my-

    super-secret-key-my-super-secret-key".getBytes());

    private static final long EXPIRATION_MS = 1000 * 60 * 60 * 24; // 1

    jwt过滤器

    拦截请求,将请求中的Authorization请求头获取到(我们返回的token),然后用token解析后的userna

    me作为依据,查询数据库即可

    直接用userName和role生成已经认证的UsernamePasswordAuthenticationToken,new方法默认

    已经设置成了已经认证了(role中包含了角色和权限了,当然)

    后面放到了SecurityContextHolder,整个认证流程结束

    /**

  • 生成JWT
  • */

    public static String generateToken(String username, Map

    Object> claims) {

    return Jwts.builder()

    .setSubject(username)

    .addClaims(claims)

    .setIssuedAt(new Date())

    .setExpiration(new Date(System.currentTimeMillis() +

    EXPIRATION_MS))

    .signWith(SECRET_KEY, SignatureAlgorithm.HS256)

    .compact();

    }

    public static Claims parseToken(String token) {

    return Jwts.parserBuilder()

    .setSigningKey(SECRET_KEY)

    .build()

    .parseClaimsJws(token)

    .getBody();

    }

    }

    /**

  • @author wuyuxinag
  • @since 2025-07-09
  • *

  • JWT 认证过滤器(过滤请求是否登录)
  • */

    public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final Set SKIP_PATHS = Set.of(

    //"/auth/**",

    "/public/**"

    );

    private final AntPathMatcher matcher = new AntPathMatcher();

    /**

  • 拦截请求,验证token
  • *

  • 正确的token格式
  • Bearer 是标准格式
  • Authorization:Bearer
  • eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b20iLCJyb2xlcyI6WyJST0xFX1VTRVIiXSwiaW

    F0IjoxNzUyMDUxODEyLCJleHAiOjE3NTIxMzgyMTJ9.HHQSqrs8_HZs2_F-

    5FpsNRJBwVOcprxtSQ7H3VimZDg

    */

    @Override

    protected void doFilterInternal(HttpServletRequest request,

    HttpServletResponse response,

    FilterChain filterChain) throws ServletException, IOException {

    String path = request.getRequestURI();

    // 放行 OPTIONS 请求

    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {

    response.setStatus(HttpServletResponse.SC_OK);

    return;

    }

    //如果token错误也能放行

    if (shouldSkip(path)){

    filterChain.doFilter(request, response);

    return; // 一定要加return,防止后续代码执行

    }

    String token = request.getHeader("Authorization");

    if (token != null && token.startsWith("Bearer ")) {

    try {

    Claims claims =

    JwtUtil.parseToken(token.replace("Bearer ", ""));

    String username = claims.getSubject();

    List roles = claims.get("roles", List.class);

    UsernamePasswordAuthenticationToken auth =

    new UsernamePasswordAuthenticationToken(

    问题:token中也没有密码如何防伪

    jwt-token的签名是我们经过密钥加密得出的,如果是篡改,自然会解析token失败

    Github-oauth2.0登录(授权码模式)

    授权码模式-适用于与三方对接的登录,如微信扫码登录

    登录步骤

    前端逻辑

    username,

    null,


    roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toLi

    st())

    );



    SecurityContextHolder.getContext().setAuthentication(auth);

    } catch (Exception e) {


    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    response.setContentType("application/json;charset=UTF-

    8");

    response.getWriter().write("{\"message\":\"Token无效或已

    过期\"}");

    return;

    }

    }

    filterChain.doFilter(request, response);

    }

    private boolean shouldSkip(String path) {

    return SKIP_PATHS.stream().anyMatch(p -> matcher.match(p,

    path));

    }

    }

    用户授权:跳转到 GitHub 授权页面。

    用户同意授权:GitHub 返回授权码code。

    获取 Access Token:使用授权码向 GitHub 请求 Access Token。

    使用 Access Token 获取用户信息:通过 Access Token 获取 GitHub 用户的详细信息。

    绑定或创建本地账户:根据 GitHub 用户信息查询并绑定本地账户,或者提示用户注册。

    点击按钮跳转到github认证界面

    github会拼上我们的回调地址,认证完成后浏览器会重定向到我们的回调界面(当然是我们的前端地

    址)

    从重定向的url中拿到code请求后端

    {

    // GitHub 登录

    const res = await

    fetch(`http://localhost:8080/auth/github/callback?

    code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`, {

    method: 'GET'

    })

    后端逻辑

    后端拿到前端请求github活动的的code,通过code请求获取accessToken,再通过accessToken获取

    用户信息,根据返回的github的用户信息判断是否在我们系统中有绑定的用户

    如果有,则登录成功(验证过程在github进行,拿到的code也是有时效性的,secret也是只有后端会存

    储,当然是选择相信)

    if (!res.ok) throw new Error('网络错误')


    const data = await res.json()

    public Response loginFromCode(String code){

    //// 1. 用前端专挑github认证的code 换 access_token

    String accessToken = getAccessToken(code);

    //// 2. 获取 GitHub 用户信息

    JSONObject githubUser = getGithubUser(accessToken); // 包含

    login, id, email 等

    System.out.println("GitHub 用户信息: " + githubUser);

    //

    // 3. 查询是否已绑定本地账号

    UserThirdPartyPo thirdPartyPo = userThirdPartyService.getOne(

    认证通过返回jwt即登录成功

    权限控制(Authorization)

    在 Spring Security 中,权限控制的核心是基于 用户身份 与 角色或权限 之间的关系来判断用户是

    否有访问某个资源的权限。

    关键概念:

    Spring Security 权限模型

    Spring Security 的权限模型通常基于以下几种方式:

    1. 基于角色(Role-Based Access Control, RBAC)

    new QueryWrapper().eq("provider",

    ThirdUserProviderEnum.GITHUB.getCode())

    .eq("third_party_id", githubUser.getString("id")));

    if (thirdPartyPo == null){

    return Response.error("未绑定本地账号,请先注册账号并绑定");

    }

    long userId = thirdPartyPo.getUserId();

    SysUserPo user = userService.getById(userId);

    if (user == null){

    return Response.error("未找到用户");

    }

    //返回token

    String jwt = JwtUtil.generateToken(user.getUsername(),

    Map.of("roles", "ROLE_USER"));

    return Response.success(jwt);

    概念

    说明

    Principal 

    当前登录用户对象,通常是 UserDetail

    s 的实例

    Authentication 

    认证信息,包括用户名、密码、权限等

    GrantedAuthority 

    授予当前用户的权限,通常是字符串,如 RO

    LE_ADMIN 、user:read 等

    SecurityContext 

    存储当前用户的 Authentication ,全局

    共享

    AccessDecisionManager 

    权限决策器,用来判断当前用户是否有权限访

    问目标资源

    用户被分配若干“角色”,如 ROLE_ADMIN 、ROLE_USER ,资源则限制只有特定角色用户可

    访问。

    2. 基于权限(Permission-Based Access Control)

    角色进一步细分为具体操作权限,如 user:add 、user:delete ,更细粒度。

    3.基于资源路径的权限控制

    通过 URL 拦截规则限制资源访问权限

    权限控制流程(简要)

    1. 用户登录后,UserDetailsService 加载用户信息与权限。

    2. 认证成功后,Authentication 存入 SecurityContextHolder 。

    3. 每次访问接口,Spring Security 的 FilterChain 会拦截请求,提取 Authenticatio

    n 。

    4. 根据访问路径与注解等,调用 AccessDecisionManager 判断是否允许访问。

    常用权限控制方式

    1. 方法级控制(推荐)

    使用注解控制权限:

    @PreAuthorize : 在方法调用前进行权限判断。

    @Secured : 类似,但语法更简单,只支持 ROLE_ 开头。

    @PostAuthorize : 在方法执行后再判断。

    ⚠️ 使用 @PreAuthorize 需要启用:@EnableGlobalMethodSecurity(prePostEna

    bled = true) 

    2. 基于 URL 的权限配置

    在配置类中自定义拦截规则:

    @PreAuthorize("hasRole('ADMIN')")

    public void adminOnly() {}

    @PreAuthorize("hasAuthority('user:delete')")

    public void deleteUser() {}

    @Secured("ROLE_USER")

    public void normalUser() {}

    @Bean

    public SecurityFilterChain filterChain(HttpSecurity http) throws

    Exception {

    权限数据存储

    通常在数据库中存储用户、角色、权限三张表:

    user (用户表)

    role (角色表)

    permission (权限表)

    通过多对多关系建立权限控制(使用中间表关联)。

    案例

    登录并设置登录人权限(其实就是再前面返回UserDetails的基础上,将权限信息添加到其中返回)

    http

    .csrf(csrf -> csrf.disable()) // 关闭CSRF(前后端分离建议关闭)

    .cors(Customizer.withDefaults())

    .authorizeHttpRequests(auth -> auth

    // 设置接口的角色权限

    .requestMatchers("/admin/**").hasRole("ADMIN")

    .anyRequest()

    .authenticated()

    )

    public UserDetails loadUserByUsername(String username) throws

    UsernameNotFoundException {

    SysUserPo sysUser =

    userMapper.selectOne(new LambdaQueryWrapper

    ().eq(SysUserPo::getUsername, username));

    if (sysUser == null) {

    throw new UsernameNotFoundException("用户不存在");

    }

    // 查询角色

    List userRoles =

    userRoleService.list(new LambdaQueryWrapper

    ().eq(UserRolePo::getUserId, sysUser.getId()));

    if (userRoles.isEmpty()) {

    log.info("用户没有角色");

    // 即便没有角色也要返回用户,权限为空

    return new

    org.springframework.security.core.userdetails.User(sysUser.getUsername(

    ), sysUser.getPassword(),

    Collections.emptyList());

    }

    // 查询角色名称列表

    List roleCodes =

    roleService.listByIds(userRoles.stream().map(UserRolePo::getRoleId).toL

    ist())

    .stream()

    .map(RolePo::getCode)

    .toList();

    if (roleCodes.isEmpty()) {

    log.info("没有查询到对应角色");

    // 即便没有角色也要返回用户,权限为空

    return new

    org.springframework.security.core.userdetails.User(sysUser.getUsername(

    ), sysUser.getPassword(),

    Collections.emptyList());

    }

    // 先构造一个包含角色的 userDetails

    List roleAuthorities =

    roleCodes.stream().map(role -> new

    SimpleGrantedAuthority("ROLE_" + role)).toList();

    UserDetails tempUser =

    new

    org.springframework.security.core.userdetails.User(sysUser.getUsername(

    ), sysUser.getPassword(),

    roleAuthorities);

    // 调用 getAuthorityUser 添加权限

    return getAuthorityUser(tempUser);

    }

    /**

  • 根据角色添加权限
  • @param user
  • @return
  • */

    public UserDetails getAuthorityUser(UserDetails user) {

    // 获取角色名(如 USER, ADMIN)

    List roles = user.getAuthorities()

    .stream()

    .map(GrantedAuthority::getAuthority)

    .filter(authority -> authority.startsWith("ROLE_"))

    .map(authority -> authority.substring(5))

    .toList();

    List rolePos = roleService.list(new

    LambdaQueryWrapper().in(RolePo::getCode, roles));

    if (rolePos.isEmpty()) {

    return user;

    deliver的请求jwt校验

    安全过滤器链-的配置类-SecurityFilterChain

    }

    List roleIds

    = rolePos.stream().map(RolePo::getId).toList();

    // 查角色对应的权限

    List rolePermissionList =

    rolePermissionService.list(

    new LambdaQueryWrapper

    ().in(RolePermissionPo::getRoleId, roleIds));

    if (rolePermissionList.isEmpty()) {

    return user;

    }

    // 批量查权限

    List permissionIds =


    rolePermissionList.stream().map(RolePermissionPo::getPermissionId).coll

    ect(Collectors.toList());

    List permissionList =

    permissionService.listByIds(permissionIds);

    if (permissionList.isEmpty()) {

    return user;

    }

    // 构建权限集合

    Set authorities = new HashSet<>();

    // 添加权限

    for (PermissionPo permission : permissionList) {

    authorities.add(new

    SimpleGrantedAuthority(permission.getCode()));

    }

    // 添加角色(作为权限)

    for (String role : roles) {

    authorities.add(new SimpleGrantedAuthority("ROLE_" +

    role));

    }

    return new

    org.springframework.security.core.userdetails.User(user.getUsername(),

    user.getPassword(), true,

    // enabled

    true, // accountNonExpired

    true, // credentialsNonExpired

    true, // accountNonLocked

    authorities);

    Spring Security 通过多个内置的 Servlet Filter 实现不同的安全功能,如认证、授权、CSR

    F、防止会话固定攻击等。

    SecurityFilterChain 负责把这些过滤器按顺序组装起来,组成一个完整的处理流程。

    配置白名单

    配置jwt来源的header是x-www-iap-assertion

    配置什么来处理jwt

    @Bean

    public SecurityFilterChain securityFilterChain(HttpSecurity http)

    throws Exception {

    http

    .sessionManagement(sessionManagement ->


    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS

    )

    )

    .csrf(AbstractHttpConfigurer::disable)

    .cors(Customizer.withDefaults())

    .authorizeHttpRequests(authorize -> authorize

    .requestMatchers(AUTH_WHITELIST).permitAll()

    .requestMatchers("/card/sync").permitAll()

    .requestMatchers("/test/**").permitAll()


    .requestMatchers("/app/teacher/getSelfFeedbackInfo").permitAll()


    .requestMatchers("/web/common/getDeliverRole/*").permitAll()


    .requestMatchers("/web/common/getDeliverRole/**").permitAll()


    .requestMatchers("/web/common/getTeamRole/*").permitAll()


    .requestMatchers("/web/common/getTeamRole/**").permitAll()


    .requestMatchers("/app/common/getMerchantWhiteList").permitAll()


    .requestMatchers("/app/common/getMerchantWhiteList/*").permitAll()

    .requestMatchers("/app/wechat/**").permitAll()


    .requestMatchers("/web/experience/meetingDetail/**").permitAll()


    .requestMatchers("/web/experience/meetingDetail").permitAll()


    .requestMatchers("/web/student/contact/info/getSubscribeQrCode").permit

    All()

    jwt解码

    如果jwt解析出现问题可以去找下configValues.getPublicKey,看下是否与生成的一致,

    configValues.getPublicKey是nacos中公共的配置

    上面配置的也就是这个解码器


    .requestMatchers("/web/experience/historyChatInfoDetail").permitAll()


    .requestMatchers("/web/common/getTeamRole/*").permitAll()


    .requestMatchers("/web/common/getTeamRole/**").permitAll()

    .requestMatchers("/xrebel/**").permitAll()


    .requestMatchers("/app/im/teacherCourseStart").permitAll()


    .requestMatchers("/app/teacher/getTeacherReviewNotStudyListNewKs").perm

    itAll()


    .requestMatchers("/web/teacher/register").permitAll()

    .anyRequest().authenticated()

    )

    //可以使用 OAuth2 Resource Server 模式,成为oauth2资源服务器

    //会自动启用 BearerTokenAuthenticationFilter,从请求中提取

    Token 并处理认证流程

    .oauth2ResourceServer(oauth2 -> oauth2

    //自定义认证失败处理器

    .authenticationEntryPoint(new

    CustomerBearerAuthenticationEntryPoint())

    //设置token的请求头

    .bearerTokenResolver(new

    HeaderBearerTokenResolver("x-www-iap-assertion"))

    //设置jwt解析

    .jwt(jwt -> jwt

    //jwt转换,及权限


    .jwtAuthenticationConverter(grantedAuthoritiesExtractor())

    //解码器

    .decoder(myCustomDecoder())

    )

    );

    return http.build();

    }

    private JwtDecoder myCustomDecoder() {

    配置jwt处理逻辑

    这个也是上面的配置grantedAuthoritiesExtractor()

    jwt解析后的claim

    jwt是三段式,其中存储用户的有效信息都在claim中,如下

    PublicKey publicKey = SecureUtil.generatePublicKey("RSA",

    Base64.decode(configValues.getPublicKey()));

    NimbusJwtDecoder build =

    NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();

    return build;

    }

    /**

  • 自定义权限转换器
  • */

    public CustomerJwtConverter grantedAuthoritiesExtractor() {

    CustomerJwtConverter customerJwtConverter = new

    CustomerJwtConverter();

    customerJwtConverter.setJwtGrantedAuthoritiesConverter(

    new GrantedAuthoritiesExtractor(dxEventBus));

    return customerJwtConverter;

    }

    {

    "cus": "{\"isDjj\":false,\"lt\":1}",

    "sub": "dxadmin",

    "role": "admin",

    "iss": "http://127.0.0.1:10111/new/security",

    "authorities": [

    "dx:deliver:all"

    ],

    "aud": "DELIVER",

    "uid": "798368854534529062",

    "nbf": 1752721709,

    "identity": "dx_znyy_resource",

    "scope": [

    "dx:deliver:all"

    ],

    "exp": 2091121709,

    "iat": 1752721709,

    "acType": 8,

    "ucode": "A0001",

    "jti": "16ffdffb-8e9a-4dd5-8bb5-36ac7ece6fe5"

    }

    jwt具体的处理逻辑

    基本就是从json中把数据取出来,如果没有就抛出异常

    返回最终认证的token(isAuthenticated(),是否认证)

    因为是已经认证的token,不会再有provider处理,后续直接从这个token中获取用户信息

    public class CustomerJwtConverter implements Converter

    AbstractAuthenticationToken> {

    private Converter>

    jwtGrantedAuthoritiesConverter =

    new JwtGrantedAuthoritiesConverter();

    @Override

    public AbstractAuthenticationToken convert(Jwt jwt) {

    Map claims = jwt.getClaims();

    Object sub = claims.get("sub");

    if (!(sub instanceof String)) {

    throw new JwtValidationException(

    "token is not valid",

    Collections.singletonList(new

    OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)));

    }

    Object uid = claims.get("uid");

    if (uid == null) {

    throw new JwtValidationException(

    "token is not valid",

    Collections.singletonList(new

    OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)));

    }

    Object role = claims.get("role");;

    if (role == null) {

    throw new JwtValidationException(

    "token is not valid",

    Collections.singletonList(new

    OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)));

    }

    Object identity = claims.get("identity");

    Object acTypeO = claims.get("acType");

    Object ucode0 = claims.get("ucode");

    Object cusO = claims.get("cus");

    String userRole = String.valueOf(role);

    String subStr = String.valueOf(sub);

    String uidStr = String.valueOf(uid);

    String ucode = ucode0 == null ? null : String.valueOf(ucode0);

    String cus = cusO == null ? null : String.valueOf(cusO);

    DxjyAuthenticationToken

    来看下这个token,在创建这个对象的时候,就设置了setAuthenticated为true,已经认证

    并且设置了需要的用户信息deliverSecurityUser

    所以返回这个token即为jwt认证的终点,后续只要从这个token中拿想要的用户信息即可

    Integer acType = acTypeO == null ? null : ((Long)

    acTypeO).intValue();

    String identityStr = identity == null ? null :

    String.valueOf(identity);

    Collection authorities =

    extractAuthorities(jwt);

    DeliverSecurityUser deliverSecurityUser = new

    DeliverSecurityUser();

    deliverSecurityUser.setId(uidStr);

    deliverSecurityUser.setUcode(ucode);

    deliverSecurityUser.setUsername(subStr);

    deliverSecurityUser.setPassword("");

    deliverSecurityUser.setIdentity(identityStr);

    deliverSecurityUser.setAuthorities(new ArrayList<>

    (authorities));

    deliverSecurityUser.setAcType(acType);

    deliverSecurityUser.setUserRole(userRole);

    if (StringUtils.isNotBlank(cus)) {


    deliverSecurityUser.setCustomParam(GsonUtils.getGson().fromJson(cus,

    CustomParam.class));

    }

    return new DxjyAuthenticationToken(authorities,

    deliverSecurityUser);

    }

    public void setJwtGrantedAuthoritiesConverter(

    Converter>

    jwtGrantedAuthoritiesConverter) {

    this.jwtGrantedAuthoritiesConverter =

    jwtGrantedAuthoritiesConverter;

    }

    protected Collection extractAuthorities(Jwt jwt)

    {

    return this.jwtGrantedAuthoritiesConverter.convert(jwt);

    }

    }

    工具类中获取当前登录用户信息

    public class DxjyAuthenticationToken extends

    AbstractAuthenticationToken {

    private final DeliverSecurityUser deliverSecurityUser;

    /**

  • Creates a token with the supplied array of authorities.
  • *

  • @param authorities the collection of GrantedAuthoritys
  • for the principal

  • represented by this authentication object.
  • */

    public DxjyAuthenticationToken(Collection

    GrantedAuthority> authorities, DeliverSecurityUser

    deliverSecurityUser) {

    super(authorities);

    super.setAuthenticated(true);

    this.deliverSecurityUser = deliverSecurityUser;

    }

    @Override

    public Object getCredentials() {

    return deliverSecurityUser;

    }

    @Override

    public Object getPrincipal() {

    return deliverSecurityUser;

    }

    }

    /**

  • 获取当前登录用户
  • *

  • @author: JZC
  • @date: 2020/6/16 15:16
  • @return:
  • com.dxznjy.framework.common.mode.security.DxjySecurityUser

    */

    public static DeliverSecurityUser getLoginUser() {

    Object principal =

    SecurityContextHolder.getContext().getAuthentication().getPrincipal();

    if (!(principal instanceof DeliverSecurityUser)) {

    开发获取当前登录人的用户信息

    token是否可以跨服务使用?

    可以,token的格式基本一致,jwt的解码key也是公共的

    如果没有特殊校验,比如会员的token可以用来访问deliver中的请求

    可以单独添加上权限控制

    添加权限

    GrantedAuthoritiesExtractor

    这个是和的解析jwt一起配置的,目的是为了配置权限

    在上面CustomerJwtConverter解析jwt过程中,获取权限会执行这份代码

    根据角色查询权限,后面又统一封拼接上字符串ROLE_ ,也就变成了角色(挺神奇的做法)

    throw new InvalidBearerTokenException("Token无效");

    }

    DeliverSecurityUser securityUser = (DeliverSecurityUser)

    principal;

    return securityUser;

    }

    DeliverSecurityUser loginUser = SpringSecurityUtils.getLoginUser();

    public class GrantedAuthoritiesExtractor implements Converter

    Collection> {

    private final DxEventBus dxEventBus;

    public GrantedAuthoritiesExtractor(DxEventBus dxEventBus) {

    this.dxEventBus = dxEventBus;

    }

    @Override

    public Collection convert(Jwt jwt) {

    Map claims = jwt.getClaims();

    Object uid = claims.get("uid");

    Object acTypeO = claims.get("acType");

    Object ucode0 = claims.get("ucode");

    Object role = claims.get("role");

    String ucode = ucode0 == null ? null : String.valueOf(ucode0);

    Response fire = dxEventBus.fire(new

    UserAccessQueryRoleEvent(ucode, ((Long) acTypeO).intValue(),

    role == null ? null : (String) role,

    String.valueOf(claims.get("jti"))));

    UserAccessQueryRoleEventHandler

    上面触发的事件,UserAccessQueryRoleEvent得到处理,这里查询具体的数据库然后返回具体的权

    限返回

    if (!(fire instanceof MultiResponse)) {

    throw new RuntimeException(fire.getErrMessage());

    }

    MultiResponse perms = (MultiResponse) fire;

    if (!perms.isSuccess()) {

    throw new RuntimeException(perms.getErrMessage());

    }

    Set grantedAuthorities = new HashSet<>();

    for (String datum : perms.getData()) {

    grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_"

    + datum));

    }

    return grantedAuthorities;

    }

    }

    @EventHandler

    public class UserAccessQueryRoleEventHandler

    implements EventHandlerI,

    UserAccessQueryRoleEvent> {

    @Autowired

    private SysRoleService sysRoleService;

    @Autowired

    private SysRolePermRepository sysRolePermRepository;

    @Autowired

    private SysPermRepository sysPermRepository;

    @Override

    @Transactional(rollbackFor = Exception.class)

    public MultiResponse execute(UserAccessQueryRoleEvent

    event) {

    if (StringUtils.isEmpty(event.getRole())) {

    return

    MultiResponse.buildFailure(CommonCodeEnum.UNAUTHORISE.code(),

    CommonCodeEnum.UNAUTHORISE.message());

    }

    Set roles = new HashSet<>();

    if(StringUtils.isBlank(event.getUserCode())){

    权限的一些问题:

    为什么按照权限来控制不管用?

    为什么@PreAuthorize("hasAuthority('m:refundReview:index')") 不管用,因为获

    取到权限后统一的前面都加上了ROLE_ 前缀,也就是说在spring-securtiy中现在只有角色而没有

    权限,所以正确的写法应该是@PreAuthorize(value = "hasRole('m:refundReview:in

    dex')") 

    人物有多角色吗?

    没有,上面获取角色只是当string来获取再查询的,如果是list肯定不兼容

    return MultiResponse.of(roles, 0);

    }

    String role = event.getRole();

    SysRole sysRole = sysRoleService.selectRoleByValue(role);

    List authVos = new ArrayList<>();

    if(ObjectUtil.isNotNull(sysRole)){

    Set perIds =

    sysRolePermRepository.findAllByRoleId(sysRole.getId()).stream()

    .map(SysRolePerm::getPermId)

    .collect(Collectors.toSet());

    List sysPermList =

    sysPermRepository.findAllById(perIds);

    if (StringUtils.equals(role,

    RoleTagEnum.JIAOFUMANAGER.getValue()) || StringUtils.equals(role,

    RoleTagEnum.ADMIN.getValue())) {

    sysPermList = sysPermRepository.findAll();

    authVos = sysPermList.stream().map(perd -> new

    AuthVo(perd.getPname(), perd.getPval())).collect(Collectors.toList());

    }

    for (SysPerm per : sysPermList) {

    //如果是超级管理员赋予所有权限

    authVos.add(new AuthVo(per.getPname(), per.getPval()));

    }

    authVos.add(new AuthVo("角色权限标识", role));

    }

    if (DxArraysUtils.isBlank(authVos)) {

    return MultiResponse.of(roles, 0);

    } else {

    roles =

    authVos.stream().map(AuthVo::getVal).collect(Collectors.toSet());

    return MultiResponse.of(roles, roles.size());

    }

    }

    }

    登录的时候也都是选角色登录的

    登录的时候是在security工程,而权限设置是在deliver工程中,security中并没有存储角色和权限,角

    色是怎么在security工程中设置的?

    登录的doLogin方法中通过dubbo接口获取登录名对应的角色

    相关表

    鼎校登录

    相关表

    统一登录

    登录的逻辑统一走的是dingxiao-service-security-authorizer-client工程

    而各种登录基本都是统一走的同一个逻辑

    统一的登录逻辑

    基本步骤

    --交付系统用户

    sys_user

    --角色员权限关联表

    sys_role_perm

    --权限表

    sys_perm

    --用户角色关联表

    sys_user_role

    --角色表

    sys_role

    --用户表(用来查询登录的具体用户)

    security_oauth2_user

    --注册的客户端

    security_oauth2_client

    各种登录,封装参数

    调用 doLogin 方法

    /oauth2/token接口

    这个 URL 是 Spring Security OAuth2 的标准 Token 接口(/oauth2/token),用于通过密码模式

    (grant_type=password)获取访问令牌(access_token)。

    通过构造这个 URL 发起 OAuth2 密码模式认证请求,最终由 Spring Security OAuth2 模块处理并

    返回 Token。

    最终会构造UsernamePasswordAuthenticationToken,然后走DaoAuthenticationProvider,后面是U

    serDetailsServiceImpl的loadUserByUsername

    请求中username是一个json的字符串,因为最终的loadUserByUsername方法的参数只有一个user

    name,所以需要的额外信息只能放一起组成json再转string

    处理一些前置逻辑,处理重写密码逻辑,获取客户端密码

    构造 OAuth2 请求(/oauth2/token)(这个路径是spring-security自带的,其实现最终会

    走到UserDetailsServiceImpl)

    发送请求获取访问令牌(access_token)

    解析响应结果

    根据用户类型调用回调方法(如学生、会员、商户等)

    返回登录结果(包含 access_token)

    public ResultUtil doLogin(UserNameJsonModel

    userNameJsonModel, String password) {

    String clientId = userNameJsonModel.getDs().getAppSource();

    // 根据client标识查询已注册的客户端

    CustomerRegisteredClient customerRegisteredClient =

    customerRegisteredClientService.findByCusClientId(clientId);

    if (customerRegisteredClient == null) {

    // 若查询不到已注册的客户端,则返回失败结果

    return ResultUtil.fail("无效的client标识");

    }

    if (StringUtils.isNotEmpty(userNameJsonModel.getRewritePwd()))

    {

    // 若存在重写密码,则将密码替换为重写密码

    password = userNameJsonModel.getRewritePwd();

    }

    // 获取客户端密码

    String clientPassword = clientPasswordMap.get(clientId);

    if (clientPassword == null) {

    // 若客户端密码为空,则使用AES加密算法对客户端秘钥进行加密后存入

    clientPasswordMap中

    byte[] decode =

    Base64.decode(customerRegisteredClient.getPlainSecret());

    clientPassword =

    SecureUtil.aes(configValues.getClientAesKeyBytes()).decryptStr(decode);

    clientPasswordMap.put(clientId, clientPassword);

    }

    // 设置请求头

    HttpHeaders requestHeaders = new HttpHeaders() ;

    requestHeaders.set("Authorization", "Basic " +

    Base64.encode(clientId + ":" + clientPassword));

    // 创建请求实体

    HttpEntity requestEntity = new HttpEntity<>

    (requestHeaders);

    // 将用户名及密码序列化为JSON字符串

    String jsonString = JSON.toJSONString(userNameJsonModel);

    // 获取客户端请求的scope

    String scpoe =

    customerRegisteredClient.getRegisteredClient().getScopes().stream().fin

    dFirst().orElseThrow(() -> new RuntimeException("client没有scope配置"));

    // 构建请求URL

    String url = String.format("http://127.0.0.1:%s%s/oauth2/token?

    grant_type=password&username=%s&password=%s&scope=%s",

    port, contextPath,

    Base64.encode(URLUtil.encode(jsonString)), password,

    URLUtil.encode(scpoe));

    // 发送POST请求获取访问令牌

    ResponseEntity postForEntity =

    restTemplate.exchange(url,

    HttpMethod.POST, requestEntity, String.class, "");

    // 若返回状态码不为200,返回失败结果

    if (!postForEntity.getStatusCode().equals(HttpStatus.OK)) {

    return ResultUtil.fail(LoginCodeEnum.LOGIN_FAIL.code(),

    LoginCodeEnum.LOGIN_FAIL.message());

    }

    // 获取返回的响应体

    String body = postForEntity.getBody();

    // 若响应体为空或无效,则返回失败结果

    if (StringUtils.isEmpty(body)) {

    return ResultUtil.fail(LoginCodeEnum.LOGIN_FAIL.code(),

    LoginCodeEnum.LOGIN_FAIL.message());

    }

    // 解析响应体,将其转换为ResultUtil对象

    ResultUtil resultUtil =

    JSON.parseObject(body, new TypeReference<>() {

    });

    // 若解析结果为空,则返回失败结果

    if (resultUtil == null) {

    return ResultUtil.fail(LoginCodeEnum.LOGIN_FAIL.code(),

    LoginCodeEnum.LOGIN_FAIL.message());

    }

    // 若解析结果中的success属性为false,则直接返回解析结果

    if (!resultUtil.isSuccess()) {

    return resultUtil;

    }

    // 获取AccessTokenResult对象

    AccessTokenResult tokenResult = resultUtil.getData();

    // 若tokenResult为空,则返回失败结果

    if (tokenResult == null) {

    return ResultUtil.fail(LoginCodeEnum.LOGIN_FAIL.code(),

    LoginCodeEnum.LOGIN_FAIL.message());

    }

    if (userNameJsonModel.getSkipNotify() != null &&

    userNameJsonModel.getSkipNotify()) {

    // 若skipNotify为true,则直接返回访问令牌结果

    return ResultUtil.success(tokenResult);

    }

    // 存入JWT令牌(可以用来取数据)

    if (StringUtils.isNotEmpty(tokenResult.getToken())) {

    SingleResponse objectSingleResponse =

    SingleResponse.buildFailure(LoginCodeEnum.LOGIN_FAIL.code(),

    LoginCodeEnum.LOGIN_FAIL.message());

    if (userNameJsonModel.getUserType() ==

    UserTypeID.STUDENT.getValue()) {

    // 若用户类型为学生,则调用学生登录成功回调方法


    if(AppSource.ZHEN_XUAN.equals(userNameJsonModel.getDs().getAppSource())

    ){

    // 如果是发起登录的源终端是甄选则需要通知甄选登录成功回调方法

    objectSingleResponse =

    zxLoginNofiyI.studentLoginSuccessCallBack(userNameJsonModel);

    }else{

    objectSingleResponse =

    znyyLoginNofiyI.studentLoginSuccessCallBack(userNameJsonModel);

    }

    } else if (userNameJsonModel.getUserType() ==

    UserTypeID.MEMBER.getValue()) {

    // 若用户类型为会员,则调用会员登录成功回调方法

    objectSingleResponse =

    znyyLoginNofiyI.userLoginSuccessCallBack(userNameJsonModel);

    } else if (userNameJsonModel.getUserType() ==

    UserTypeID.MERCHANT.getValue()) {

    // 若用户类型为商户,则调用管理员登录成功回调方法

    objectSingleResponse =

    znyyLoginNofiyI.adminLoginSuccessCallBack(userNameJsonModel);

    } else if (userNameJsonModel.getUserType() ==

    UserTypeID.DELIVER.getValue()) {

    // 若用户类型为交付,则调用交付登录成功回调方法

    objectSingleResponse =

    deliverLoginUserCall.userLoginSuccessCallBack(userNameJsonModel);

    } else if (userNameJsonModel.getUserType() ==

    UserTypeID.ZHEN_XUAN_ADMIN.getValue()){

    // 若用户类型为甄选后台用户,则调用甄选后台成功回调方法

    objectSingleResponse =

    zxAdminNofiyI.userLoginSuccessCallBack(userNameJsonModel);

    }else if(userNameJsonModel.getUserType() ==

    UserTypeID.SCRM.getValue()){

    // 若登录来源是SCRM系统,则调用Scrm登录成功回调,验证图片验证码

    // znyyLoginNofiyI =

    DubboCallUtil.getProxy(LoginNotifyI.class, AppSource.SCRM, false);

    objectSingleResponse =

    scrmLoginNofiyI.userLoginSuccessCallBack(userNameJsonModel);

    } else if (userNameJsonModel.getUserType() ==

    UserTypeID.TRAIN.getValue()) {

    objectSingleResponse =

    trainLoginNotifyI.userLoginSuccessCallBack(userNameJsonModel);

    } else if(userNameJsonModel.getUserType() ==

    UserTypeID.SCRM_V2.getValue()){

    // 若登录来源是SCRM系统,则调用Scrm登录成功回调,验证图片验证码

    // znyyLoginNofiyI =

    DubboCallUtil.getProxy(LoginNotifyI.class, AppSource.SCRM, false);

    objectSingleResponse =

    scrmLoginV2NofiyI.userLoginSuccessCallBack(userNameJsonModel);

    }else if(userNameJsonModel.getUserType() ==

    UserTypeID.IM.getValue()){

    //todo ()im 登录回调处理

    objectSingleResponse =

    imLoginV2NofiyI.userLoginSuccessCallBack(userNameJsonModel);

    }

    // 若回调方法返回结果不成功,则返回失败结果

    if (!objectSingleResponse.isSuccess()) {

    return

    ResultUtil.response(DxResponseUtils.changeData(null,

    objectSingleResponse));

    }

    // 若回调方法成功并返回了数据,则将其存入tokenResult的回调数据中

    oauth2/token后续处理逻辑

    前面得知登录其实都是走的统一的接口,然后又调用oauth2/token这个统一的oauth2.0登录接口,返

    回token

    所以接下来分下这个aouth2.0的处理流程

    authorizationServerSecurityFilterChain

    配置用于oauth2流程的处理逻辑的过滤器(虽然spring-security帮我们实现了接口,但是具体实现要

    我们要配置并实现)

    这个 SecurityFilterChain 使用了 OAuth2AuthorizationServerConfigurer,它是 Spring Security 官

    方提供的专门用于配置 OAuth2 授权服务器的配置器,只处理 OAuth2 相关的请求

    其中使用了PasswordAuthenticationConverter来处理http请求

    if (objectSingleResponse.isSuccess() &&

    objectSingleResponse.getData() != null) {


    tokenResult.setCallbackData(objectSingleResponse.getData());

    }

    }

    @Bean

    @Order(1)

    public SecurityFilterChain

    authorizationServerSecurityFilterChain(HttpSecurity http,

    AuthenticationManager authenticationManager,


    OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator

    > tokenGenerator) throws Exception {

    OAuth2AuthorizationServerConfigurer

    authorizationServerConfigurer = new

    OAuth2AuthorizationServerConfigurer();


    authorizationServerConfigurer.tokenEndpoint(oAuth2TokenEndpointConfigur

    er -> {

    oAuth2TokenEndpointConfigurer.

    accessTokenRequestConverters(authenticationConverters -

    > authenticationConverters.add(new PasswordAuthenticationConverter()))

    .authenticationProviders(authenticationProviders ->

    authenticationProviders.add(new

    PasswordAuthenticationProvider(authenticationManager,

    authorizationService, tokenGenerator)))

    .accessTokenResponseHandler(new

    MyAuthenticationSuccessHandler())

    .errorResponseHandler(new

    MyAuthenticationFailureHandler());

    PasswordAuthenticationConverter

    再看下这个converter的逻辑,就是根据request对象,生成PasswordAuthenticationToken

    后面也是熟悉的逻辑根据这个token类型寻找对应的provider,然后处理token,设置用户信息授权等

    });

    http.apply(authorizationServerConfigurer);

    http.cors().disable().csrf().disable();

    http.authorizeHttpRequests(auth -> auth

    .requestMatchers(AUTH_WHITELIST).permitAll()

    .anyRequest().authenticated());

    return http.build();

    }

    public class PasswordAuthenticationConverter implements

    AuthenticationConverter {

    @Override

    public Authentication convert(HttpServletRequest request) {

    String grantType =

    request.getParameter(OAuth2ParameterNames.GRANT_TYPE);

    if

    (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {

    return null;

    }

    Authentication authentication =

    SecurityContextHolder.getContext().getAuthentication();

    MultiValueMap queryParams =

    RequestUtils.getQueryParams(request);

    String scope =

    queryParams.getFirst(OAuth2ParameterNames.SCOPE);

    if (StringUtils.isEmpty(scope)) {


    OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_SCOPE, "scope为

    空",

    scope);

    }

    Set requestedScopes = new HashSet<>

    ((StrUtil.split(scope, " ")));

    // 用户名验证(必需)

    String username =

    queryParams.getFirst(OAuth2ParameterNames.USERNAME);

    if (StrUtil.isBlank(username)) {

    OAuth2EndpointUtils.throwError(


    LoginCodeEnum.USERNAME_PASSWORD_IS_NOT_VALID.code().toString(),

    UserDetailsServiceImpl

    之后还是转换成了UsernamePasswordAuthenticationToken,走到了熟悉的UserDetailsServiceImp

    l

    根据不同方式选择不同登录的具体实现,但是统一都是返回userDetails


    LoginCodeEnum.USERNAME_PASSWORD_IS_NOT_VALID.message(),

    "用户名为空"

    );

    }

    // 密码验证(必需)

    String password =

    queryParams.getFirst(OAuth2ParameterNames.PASSWORD);

    if (StrUtil.isBlank(password)) {

    OAuth2EndpointUtils.throwError(


    LoginCodeEnum.USERNAME_PASSWORD_IS_NOT_VALID.code().toString(),


    LoginCodeEnum.USERNAME_PASSWORD_IS_NOT_VALID.message(),

    username

    );

    }

    // 附加参数(保存用户名/密码传递给 PasswordAuthenticationProvider 用

    于身份认证)

    Map additionalParameters = queryParams

    .entrySet()

    .stream()

    .filter(e ->

    !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&

    !e.getKey().equals(OAuth2ParameterNames.SCOPE)

    ).collect(Collectors.toMap(Map.Entry::getKey, e ->

    e.getValue().get(0)));

    return new PasswordAuthenticationToken(authentication,

    requestedScopes, additionalParameters);

    }

    @Override

    public UserDetails loadUserByUsername(String username) throws

    UsernameNotFoundException {

    if (StringUtils.isEmpty(username)) {


    OAuth2EndpointUtils.throwError(LoginCodeEnum.USERNAME_PASSWORD_IS_NOT_V

    ALID.code().toString(), LoginCodeEnum.ACCOUNT_NO_LOGIN_ROLE.message(),

    "");

    查询security_oauth2_user,得到用户

    查看其中一个逻辑,就是去数据库查询用户,组装返回userDetails对象

    得到SecurityOauth2User,后面对它进行封装成userDetails,进行spring-security的校验,通过即登录

    成功

    根据这个查询方法,也解答了一个问题,为什么userType不同,同样的密码为什么登录不上,因为在系

    统中记录就不是"同一个人"

    }

    String decodeStr = Base64.decodeStr(username);

    decodeStr = URLUtil.decode(decodeStr);

    if (!JSON.isValid(decodeStr)) {


    OAuth2EndpointUtils.throwError(LoginCodeEnum.LOGIN_PARAMS_ERROR.code().

    toString(), LoginCodeEnum.LOGIN_PARAMS_ERROR.message(), username);

    }

    UserNameJsonModel nameJsonModel =

    JSON.parseObject(decodeStr, UserNameJsonModel.class);

    CustomerUserDetails userDetails = null;

    // 用户微信用户名密码登录

    if (nameJsonModel.getCustomParam().getLt() == 4) {

    userDetails = this.doThirdPasswordLogin(nameJsonModel);

    } else if (nameJsonModel.getCustomParam().getLt() == 5) {

    userDetails = this.doStudentMemberPwdLogin(nameJsonModel);

    } else if (nameJsonModel.getCustomParam().getLt() == 6) {

    /*用户使用第三方授权登陆,无用户名无密码*/

    userDetails =

    this.doAppNoUserNameAndNoPwdLogin(nameJsonModel);

    } else if

    (StringUtils.isNotBlank(nameJsonModel.getRewritePwd())) {

    userDetails =

    this.getUserDetailsUseRewritePwd(nameJsonModel);

    } else if (StringUtils.isNotBlank(nameJsonModel.getMemName()))

    {

    userDetails = getUserDetailsUseUseMemberPwd(nameJsonModel);

    } else {

    userDetails = getUserDetails(nameJsonModel);

    }

    private SecurityOauth2User getSecurityOauth2User(

    String username, Integer userType) {

    MultiResponse securityOauth2Users =

    securityOauth2UserService.list(username, userType, null);

    if (!securityOauth2Users.isSuccess()) {


    OAuth2EndpointUtils.throwError(CommonCodeEnum.SERVER_ERROR.code().toStr

    在返回userDetails后,也就是spring-security的校验了

    查询各系统中用户信息

    调用buddo接口查询各系统中用户信息,其中包括userCode和role

    这个接口是多实现的,根据传入的参数,动态判断,所属 用户是属于哪个系统的

    后面会将这部分放到userDetails中

    各种登录有什么区别?

    看完上面得知各种登录其实都走的统一的dologin 方法,而不同的登录也就是调用这个方法也就是

    组装的参数不同

    后面登录逻辑是根据不同参数走不同的分支

    也就是说,找出关键参数,也就能区分各种登录

    userType

    在security工程中根据username和userType找人

    这个也决定了去哪个系统中找对应的人

    ing(), securityOauth2Users.getErrMessage(), username);

    }

    List securityOauth2UsersData =

    securityOauth2Users.getData();

    if (securityOauth2UsersData == null ||

    securityOauth2UsersData.isEmpty()) {


    OAuth2EndpointUtils.throwError(LoginCodeEnum.ACCOUNT_DOES_NOT_EXIST.cod

    e().toString(), LoginCodeEnum.ACCOUNT_DOES_NOT_EXIST.message(),

    username);

    }

    SecurityOauth2User oauth2User =

    Objects.requireNonNull(securityOauth2UsersData).get(0);

    return oauth2User;

    }

    LoginUserCallI loginUserCallI =

    DubboCallUtil.getProxy(LoginUserCallI.class, group, false);

    SingleResponse checkUserHasRole =

    loginUserCallI.getUserRoleByLoginName(nameJsonModel.getUname(),

    nameJsonModel.getCustomParam().getRoleSec());

    MultiResponse securityOauth2Users =

    securityOauth2UserService.list(username, userType, null);

    可以看一个案例

    userDetailServiceImpl 中根据userType判断找对应的实现类(后面这个实现类获取角色),

    很明显这个会请求deliver的dubbo接口

    DxSource

    组成分别是

    appSource-应用来源

    terminalSource-终端来源

    viewSource-呈现终端

    doLogin中根据appSource找注册的客户端,后面会构造请求头,设置scope

    也有直接用appSource来找具体的实现类(请求各系统中用户信息)

    public enum UserTypeID {

    STUDENT(6, "学员"),

    MERCHANT(8, "商户"),

    MEMBER(9, "会员"),

    DELIVER(7, "交付系统"),

    SCRM(10, "SCRM系统"),

    SCRM_V2(11, "SCRM_V2系统"),

    ZHEN_XUAN_ADMIN(12, "甄选后台"),

    TRAIN(13, "学习中心"),

    IM(14, "IM系统后台")

    if (nameJsonModel.getUserType() == UserTypeID.DELIVER.getValue() &&(

    nameJsonModel.getCustomParam().getMeetingUserType() == 2)){

    LoginUserCallI loginUserCallI =

    DubboCallUtil.getProxy(LoginUserCallI.class, AppSource.DELIVER,

    false);

    DELIVER##WX##MINIAPP

    String clientId = userNameJsonModel.getDs().getAppSource();

    // 根据client标识查询已注册的客户端

    CustomerRegisteredClient customerRegisteredClient =

    customerRegisteredClientService.findByCusClientId(clientId);

    }else if (nameJsonModel.getDs().getAppSource().equals(AppSource.IM)) {

    group = nameJsonModel.getDs().getAppSource();

    }

    CustomParam

    其中重点关注下lt 参数和role 参数,控制着登录方式

    如果lt不同,则,登录的逻辑不同

    if (StringUtils.isNotEmpty(nameJsonModel.getRole())) {

    LoginUserCallI loginUserCallI =

    DubboCallUtil.getProxy(LoginUserCallI.class, group, false);

    public class CustomParam implements Serializable {

    @Tolerate

    public CustomParam() {

    }

    /**

  • 设备id
  • */

    private String dd;

    /**

  • 如果是时长登陆就是学员所属门店code
  • */

    private String mc;

    /**

  • 登录方式 1用户密码登录 2、短信验证码登录 3.小程序授权无秘登录 4、小程序用户
  • 名密码登录 5学员使用会员账户登录,6 app调用第三方绑定授权登陆

    */

    private Integer lt;

    /**

  • 第三方openid
  • */

    private String wxo;


    ......(省略)

    }

    if (nameJsonModel.getCustomParam().getLt() == 4) {

    userDetails = this.doThirdPasswordLogin(nameJsonModel);

    } else if (nameJsonModel.getCustomParam().getLt() == 5) {

    userDetails = this.doStudentMemberPwdLogin(nameJsonModel);

    } else if (nameJsonModel.getCustomParam().getLt() == 6) {

    /*用户使用第三方授权登陆,无用户名无密码*/

    userDetails =

    this.doAppNoUserNameAndNoPwdLogin(nameJsonModel);

    } else if

    (StringUtils.isNotBlank(nameJsonModel.getRewritePwd())) {


    本文整理自实践经验,如有问题欢迎交流讨论。


    评论