Spring Security实现单点登录SSO(一)

目录

一、简介        

二、配置类

三、UserDetailsService接口的实现类

四、过滤器

五、登录业务流程

六、测试


一、简介        

        本文主要讲解了利用Spring Security框架,实现单点登录的流程。

        Demo项目是基于RBAC模型设计的数据表,并采用MyBatis Plus实现基本的数据增删改查功能,默认已完成该操作。(附:数据表SQL语句)

        单点登录的英文名叫做:Single Sign On(简称sso),指在同一帐号平台下的多个应用系统中,用户只需登录一次,即可访问所有相互信任的系统。特别适合微服务项目。

        本文为笔者原创,如有转载请注明出处!话不多说,直接上流程!

二、配置类

        首先,添加相关依赖,如下:

        <!--SpringMvc 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Security依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- FastJson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.21</version>
        </dependency>
        <!-- JWT依赖(Java JWT)-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

         在项目中,添加config包,并在其中添加配置类 SecurityConfiguration.java,继承 WebSecurityConfigurerAdapter 类并重写configure方法。

        注意:需要开启权限验证,必须在启动类或者配置类中添加注解。笔者是在配置类添加的:@EnableGlobalMethodSecurity(prePostEnabled = true)

如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

     @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http);

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

        // 同一个路径配置多个规则,以第一个为准
        String[] permitUrls = new String[]{
                "/user/login", //登录请求路径
                "/doc.html",
                "/**/*.js",
                "/**/*.css",
                "/favicon.ico",
                "/swagger-resources",
                "/v2/api-docs"
        };
        http.authorizeRequests()
                .mvcMatchers(permitUrls) 
                .permitAll() // 即上面的路径数组,不需要鉴权即可访问
                .anyRequest() // 除上面的路径外,都需要鉴权
                .authenticated();

        // 是否启用登录表单
        // http.formLogin();

    }

}

        如果我们不添加密码编辑器,那么就会使用明文密码,这是不安全的,所以,我们需要在配置类将密码编辑器的bean注册到上下文容器中,如下: 

    @Bean
    public PasswordEncoder pwEncoder() {
        return new BCryptPasswordEncoder();
    }

        配置类中,添加认证管理器的bean对象到上下文容器中,如下:

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

        到这里,配置类就暂时写完了(ps:后续需要将过滤器加入到过滤器链)

三、UserDetailsService接口的实现类

        首先,需要封装一个LoginUserDetail对象,继承 Spring Security框架核心包中的一个User类,该对象主要是封装了登录用户相关的信息。注意:构造器必须调用父类的构造器,将相关信息封装在里面。

package com.example.sso.secutity;

import lombok.Getter;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;


/**
 * 登录验证对象(用户名、密码)
 */
@Getter
@Accessors(chain = true)
public class LoginUserDetail extends User {
    private Long userId;

    /**
     * 构造器
     *
     * @param username    用户名
     * @param password    密码
     * @param enabled     是否启用
     * @param authorities 权限集合
     */
    public LoginUserDetail(Long userId, String username, String password, boolean enabled, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, true, true, true, authorities);
        this.userId = userId;
    }
}

         UserDetailsService接口是Spring Security框架核心包中的一个接口,实现该接口需要重写loadUserByUsername(String s)方法,方法的返回值即上面的LoginUserDetail,作用是为认证管理器提供用户信息。

package com.example.sso.secutity;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.sso.pojo.entity.User;
import com.example.sso.service.IUserService;
import io.jsonwebtoken.lang.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;


@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    IUserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
        Assert.isTrue(user != null, "用户信息不存在");
        // 根据用户名,查询权限列表
        List<String> permissions = userService.listPermissionsByUser(user);
        // 将权限从String转为GrantedAuthority
        List<GrantedAuthority> authorities = permissions.stream().
                map(item -> (GrantedAuthority) new SimpleGrantedAuthority(item)).collect(Collectors.toList());
        return new LoginUserDetail(user.getId(), username, user.getPassword(), user.getEnable(), authorities);
    }
}

四、过滤器

        过滤器的作用是,当接收到外部的请求时,会先对请求进行一系列的处理之后再放行,之后才会转到Controller层进行处理。所以,我们使用过滤器对每个请求的请求头携带的jwt信息进行检测,并且解析出权限信息封装到上下文中。

        在创建过滤器之前,先创建一个 LoginPrincipal 类,作用是封装登录用户的信息。如下:

package com.example.sso.secutity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;


/**
 * 当事人信息对象
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginPrincipal implements Serializable {
    private Long userId;
}

        新建filter包,新建 JwtHandlerFilter 过滤器类,继承OncePerRequestFilter类,并强制重写doFilterInternal方法。

        重写的方法主要做以下几件事:1.获取请求头的jwt信息;2.解析jwt,获得权限信息;3.将权限信息封装到上下文中。代码如下:

package com.example.sso.filter;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.example.sso.secutity.LoginPrincipal;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;


/**
 * jwt处理过滤器
 */
@Component("jwtHandlerFilter")
@Slf4j
public class JwtHandlerFilter extends OncePerRequestFilter {
    @Value("${jwt.secret-key}")
    private String secretKey;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 删除上下文信息,避免线程安全问题
        SecurityContextHolder.clearContext();

        String jwt = request.getHeader("Authorization");
        log.debug("获取客户端携带的token:{}", jwt);
        if (StringUtils.isBlank(jwt) ) {
            // 对于无效的JWT,直接放行,交由后续的组件进行处理
            log.debug("获取到的token被视为无效,当前过滤器将放行...");
            filterChain.doFilter(request, response);
            return;
        }

        // 尝试解析JWT
        Claims claims;
        Long userId;
        String authorities;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(jwt)
                    .getBody();

            userId = claims.get("User-Id", Long.class);
            authorities = claims.get("Authorities", String.class);
            log.info("从jwt获取到权限信息:{}", authorities);

        } catch (ExpiredJwtException e) {
            // 这个异常是jwt有效期过了
            log.debug("解析JWT时出现ExpiredJwtException");
            e.printStackTrace();

            response.setContentType("text/html; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.println("用户登录信息过期,请重新登录");
            return;
        } catch (MalformedJwtException e) {
            // 这个异常是解析jwt失败
            log.debug("解析JWT时出现MalformedJwtException");
            e.printStackTrace();

            response.setContentType("text/html; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.println("用户登录信息过期,请重新登录");
            return;
        } catch (SignatureException e) {
            // 这个异常是解析jwt时签名错误
            log.debug("解析JWT时出现SignatureException");
            e.printStackTrace();

            response.setContentType("text/html; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.println("用户登录信息过期,请重新登录");
            return;
        } catch (Throwable e) {
            log.debug("解析JWT时出现其他异常");
            e.printStackTrace();

            response.setContentType("text/html; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.println("用户登录信息过期,请重新登录");
            return;
        }

        // 处理权限信息
        List<SimpleGrantedAuthority> grantedAuthorities
                = JSON.parseArray(authorities, SimpleGrantedAuthority.class);
        // 登录当事人信息
        LoginPrincipal loginPrincipal = new LoginPrincipal(userId);
        // 创建Authentication对象(用户验证对象)
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                loginPrincipal, null, grantedAuthorities);

        // 将Authentication对象存入到 SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 过滤器链继续向后传递,即:放行
        filterChain.doFilter(request, response);

    }

}

        到此,过滤器就创建完成了。还记得之前说的,需要在配置类中把过滤器加入到过滤器链中吗?因此,完整的配置类代码如下:

package com.example.sso.config;

import com.example.sso.filter.JwtHandlerFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    JwtHandlerFilter jwtHandlerFilter;

    @Bean
    public PasswordEncoder pwEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http);

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

        // 同一个路径配置多个规则,以第一个为准
        String[] permitUrls = new String[]{
                "/user/login",
                "/doc.html",
                "/**/*.js",
                "/**/*.css",
                "/favicon.ico",
                "/swagger-resources",
                "/v2/api-docs"
        };
        http.authorizeRequests()
                .mvcMatchers(permitUrls)
                .permitAll()
                .anyRequest()
                .authenticated();

        // 是否启用登录表单
        // http.formLogin();

        // 添加过滤器
        http.addFilterBefore(jwtHandlerFilter, UsernamePasswordAuthenticationFilter.class);


    }


}

五、登录业务流程

        直接将Controller和Service层的代码展示出来,如下:

        UserController类:

package com.example.sso.controller;

import com.alibaba.fastjson.JSONObject;
import com.example.sso.pojo.dto.LoginDTO;
import com.example.sso.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * 用户表
 *
 * @author darko
 * @date 2023-08-27
 */
@RestController
@Slf4j
@RequestMapping("/User")
public class UserController {
    @Autowired
    IUserService userService;

    @PostMapping("/user/login")
    public JSONObject login(@RequestBody LoginDTO dto) {
        String jwt = userService.login(dto);
        return jwt;
    }

}

         LoginDTO 类:

package com.example.sso.pojo.dto;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;


@Data
@Accessors(chain = true)
public class LoginDTO {
    /**
     * 用户名
     */
    @ApiModelProperty(value = "用户名")
    private String username;
    /**
     * 密码
     */
    @ApiModelProperty(value = "密码")
    private String password;
}

         IUserService 接口:

package com.example.sso.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.sso.pojo.dto.LoginDTO;
import com.example.sso.pojo.entity.User;

import java.util.List;


/**
 * 用户表
 *
 * @author darko
 * @date 2023-08-27
 */
public interface IUserService extends IService<User> {
    String login(LoginDTO dto);
    List<String> listPermissionsByUser(User user);
}

        UserServiceImpl 类:

package com.example.sso.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.sso.mapper.UserMapper;
import com.example.sso.pojo.dto.LoginDTO;
import com.example.sso.pojo.entity.Permission;
import com.example.sso.pojo.entity.RolePermission;
import com.example.sso.pojo.entity.User;
import com.example.sso.secutity.LoginUserDetail;
import com.example.sso.service.IPermissionService;
import com.example.sso.service.IRolePermissionService;
import com.example.sso.service.IRoleService;
import com.example.sso.service.IUserService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;


/**
 * 用户表
 *
 * @author darko
 * @date 2023-08-27
 */
@Primary
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Autowired
    AuthenticationManager authenticationManager;
    @Value("${sso.jwt.secret-key}")
    private String secretKey;
    @Autowired
    IRoleService roleService;
    @Autowired
    IRolePermissionService rolePermissionService;
    @Autowired
    IPermissionService permissionService;

    @Override
    public String login(LoginDTO dto) {
        // 检验用户信息
        String username = dto.getUsername();
        String password = dto.getPassword();
        Assert.isTrue(StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)
                , "用户名或密码错误");
        User user = this.getOne(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
        Assert.isTrue(user != null, "用户信息不存在");

        // 交给认证管理器去验证密码,如果认证不通过,会抛出异常 AuthenticationException
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, password);
        Authentication authResult = authenticationManager.authenticate(authentication);
        log.debug("登录通过,认证管理器返回:{}", authResult);

        // 认证通过,封装 jwt
        LoginUserDetail userDetails = (LoginUserDetail) authResult.getPrincipal();
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("User-Id", userDetails.getUserId());
        claims.put("Authorities", JSON.toJSONString(userDetails.getAuthorities()));

        String jwt = Jwts.builder()
                // Header
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "JWT")
                // Payload
                .setClaims(claims)
                // Signature
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();

        log.info("登录成功,返回jwt:{}", jwt);
        return jwt;
    }

    @Override
    public List<String> listPermissionsByUser(User user) {
        Assert.isTrue(user.getRoleId() != null, "用户角色不存在");
        return permissionService.list(
                new LambdaQueryWrapper<Permission>().in(
                        Permission::getId,
                        rolePermissionService.list(new LambdaQueryWrapper<RolePermission>().eq(RolePermission::getRoleId, user.getRoleId()))
                                .stream().map(RolePermission::getPermissionId).collect(Collectors.toList())
                )
        ).stream().map(Permission::getValue).collect(Collectors.toList());
    }
}

六、测试

        至此,所有的流程都走完了。接下来,让我们来测试下效果。

        在UserController类中添加一个测试方法,如下:

    @GetMapping("/test")
    @PreAuthorize("hasAuthority('/admin')")
    public String test() {
        return "测试成功";
    }

        用postman测试,首先登录用户获取jwt

         将获取到的jwt,添加到请求头,发起测试请求,结果显示:测试成功!

        以上就是Spring Security框架实现单点登录的流程,Demo项目可以在此获取,选择master分支即可下载。

        如有错误之处,敬请指正! 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值