草庐IT

springboot 2.7整合spring security 5.7整合jwt实现用户登录注册与鉴权全记录

ricardo.M.Yu 2023-04-11 原文

目录​​​​​​​

前言

token与 jwt  (JSON Web Token)介绍

JWT 的原理

JWT 的数据结构

​编辑

Header

 Payload

Signature

JWT 工具类

spring security简介

用户认证(Authentication)

用户授权(Authorization)

过滤器链

核心组件

AuthenticationManager

SecurityContextHolder

PasswordEncoder

UserDetails

UserDetailsService

BasicAuthenticationFilter

AuthenticationEntryPoint

登录流程图

集成流程

集成spring security

maven依赖 

WebSecurityConfig 配置

授权相关配置文件  AuthProperties

配置文件 application-security.yaml

异常处理UserAuthenticationEntryPoint

JWTService 与  JWTServiceImpl

SysUserService

用户信息

测试

登录接口

登陆失败提示


前言

关于系统最终想实现的功能:使用token来实现登录校验,用户登录后拿到token,然后将token放入httpHeader,之后每次接口请求都携带token,验证成功才可进行正常访问流程

储备知识

token与 jwt  (JSON Web Token)介绍

JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{
  "姓名": "张三",
  "角色": "管理员",
  "到期时间": "2018年7月1日0点0分"
}

JWT 的数据结构

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

Header(头部)

Payload(负载)

Signature(签名)

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT

最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。

 Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

 HMACSHA256(  base64UrlEncode(header) + "." +  base64UrlEncode(payload),  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

JWT 工具类

有很多常见的工具类,我这边用的是这个

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

spring security简介

介绍的文章一抓一大把,这边主要说一下他的几个核心东西

用户认证(Authentication)

是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。

用户授权(Authorization)

发生在 Authentication(认证)之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。

过滤器链

核心组件

AuthenticationManager

SecurityContextHolder

PasswordEncoder

UserDetails

UserDetailsService

BasicAuthenticationFilter

AuthenticationEntryPoint

登录流程图

集成流程

集成spring security

maven依赖 

springboot版本 2.7.3 springsecurity 版本 5.7.3

<!-- spring-boot -->
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.3</version>
        <relativePath/>
</parent>

<dependencies>
<!-- spring security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

WebSecurityConfig 配置

注意,springsecurity 5.7之后配置方式已经优化,无需再使用继承式的配置,直接bean方式配置即可

@EnableWebSecurity
@EnableConfigurationProperties(AuthProperties.class)
public class WebSecurityConfig {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private AuthProperties authProperties;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private JWTService jwtService;
    @Autowired
    private CacheManager cacheManager;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                // 基于 token,不需要 csrf
                .csrf().disable()
                // 基于 token,不需要 session
                  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 下面开始设置权限
                .authorizeRequests(authorize -> authorize
                        .antMatchers(authProperties.getPermitStatic().toArray(new String[0])).permitAll()
                        .antMatchers(authProperties.getPermitMethod().toArray(new String[0])).permitAll()
                        // 其他地址的访问均需验证权限
                        .anyRequest().authenticated())
                .addFilter(new JWTAuthenticationFilter(authenticationManager, sysUserService, jwtService, userCache()))
                .exceptionHandling().authenticationEntryPoint(new UserAuthenticationEntryPoint()).and()
                // 认证用户时用户信息加载配置,注入springAuthUserService
                .userDetailsService(sysUserService).build();
    }


    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    /**
     * 密码明文加密方式配置(使用国密SM4)
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new SM4PasswordEncoder();
    }

    @Bean
    UserCache userCache(){
        Cache ca = cacheManager.getCache("userCache");
        return new SpringCacheBasedUserCache(ca);
    }

授权相关配置文件  AuthProperties

@Data
@ConfigurationProperties(prefix = "lc.security")
public class AuthProperties {

    private JWT jwt;

    private List<String> permitStatic;

    private List<String> permitMethod;

    @Data
    public static class JWT{

        private Claims claims = new Claims();
        private String authHeader;
        private String secret;
        private Type type = Type.RANDOM;

        public void setAuthHeader(String authHeader) {
            this.authHeader = authHeader;
        }

        public String getAuthHeader() {
            return authHeader;
        }

        public Claims getClaims() {
            return claims;
        }

        public void setSecret(String secret) {
            this.secret = secret;
        }

        public String getSecret() {
            return secret;
        }

        public void setType(Type type) {
            this.type = type;
        }

        public Type getType() {
            return type;
        }

        public enum Type {
            RANDOM, FOREVER
        }

        @Setter
        @Getter
        public static class Claims {
            private String issuer = "AppName";
            private String audience = "Web";
            private String subject = "Auth";
            private Long expirationTimeMinutes = 60L;
        }

    }

配置文件 application-security.yaml

lc:
  security:

    #静态资源放行
    permit-static:
      - /*.html
      - /*.html
      - /favicon.ico
      - /**/*.html
      - /**/*.css
      - /**/*.js
      - /**/*.png
      - /**/*.jpg
      - /**/*.ttf
      - /**/*.woff
      - /**/*.wav
      - /**/*.gif
      - /swagger-ui.html

    #方法放行
    permit-method:
      - /swagger-resources
      - /v2/api-docs
      - /v3/api-docs
      - /api/v1/sys/auth/login

    jwt:
      auth-header: Authorization
      secret: mySecret
      type: forever
      claims:
        issuer: LC
        audience: Web
        subject: Auth
        expiration-time-minutes: 3000

异常处理UserAuthenticationEntryPoint

public class UserAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.OK.value());
        response.getWriter().write(JSONObject.toJSONString(ReturnVO.failed("登录信息不正确!")));
        response.getWriter().flush();

    }
}

过滤器JWTAuthenticationFilter

@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    private final SysUserService userService;
    private final JWTService jwtService;
    private final UserCache userCache;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, SysUserService userService, JWTService jwtService, UserCache userCache) {
        super(authenticationManager);
        this.userService = userService;
        this.jwtService = jwtService;
        this.userCache = userCache;
    }


    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = jwtService.getToken(request);
        String tokenHeader = request.getHeader("Authorization");
        // 如果请求头中没有Authorization信息则直接放行了
        if (!StringUtils.hasLength(tokenHeader)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        String username = jwtService.validateToken(token);
        if (!StringUtils.hasLength(username)) {
            log.error("从token中未获取到用户名, token:{}, URI:{}", token, request.getServletPath());
            chain.doFilter(request, response);
            return;
        }

        //从缓存中验证token的存在性
        UserInfo user = (UserInfo) userCache.getUserFromCache(username);
        if (null == user) {
            try {
                user = (UserInfo) userService.loadUserByUsername(username);
                userCache.putUserInCache(user);
            } catch (UsernameNotFoundException e) {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setStatus(HttpStatus.OK.value());
                response.getWriter().write(JSONObject.toJSONString(ReturnVO.failed(e.getMessage())));
                response.getWriter().flush();
                return;
            }
        }
        // 如果从持久化存储中仍未查到,则执行后续操作,最后返回用户不存在信息到前端
        if (null != user) {
            // 清空“密码”属性
            // 创建验证通过的令牌对象
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            // 设置令牌到安全上下文中
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

JWTService 与  JWTServiceImpl

public interface JWTService {

    /**
     * 签名生成
     * @param username
     * @return
     */
    String generateToken(String username);
    /**
     * 签名检验
     * @param token
     * @return
     */
    String validateToken(String token);
    /**
     * 签名查询
     * @param request
     * @return
     */
    String getToken(HttpServletRequest request);
}

@Slf4j
@Service
public class JWTServiceImpl implements JWTService {

    private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;

    private AuthProperties properties;

    public JWTServiceImpl(AuthProperties properties) {
        this.properties = properties;
    }

    private Claims getAllClaims(String token) throws AuthTokenException {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(properties.getJwt().getSecret())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            throw new AuthTokenException(e.getMessage());
        }
        return claims;
    }

    private Date generateExpirationDate() {
        return new Date(new Date().getTime() + properties.getJwt().getClaims().getExpirationTimeMinutes() * 60 * 1000);
    }

    private String getAuthHeader(HttpServletRequest request) {
        return request.getHeader(properties.getJwt().getAuthHeader());
    }

    @Override
    public String generateToken(String username) {
        return Jwts.builder()
                .setIssuer(properties.getJwt().getClaims().getIssuer())
                .setSubject(username)
                .setAudience(properties.getJwt().getClaims().getAudience())
                .setIssuedAt(new Date())
                .setExpiration(generateExpirationDate())
                .signWith(SIGNATURE_ALGORITHM, properties.getJwt().getSecret())
                .compact();
    }

    @Override
    public String validateToken(String token){
        Claims allClaims = null;
        try {
            return getAllClaims(token).getSubject();
        } catch (AuthTokenException e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }

    @Override
    public String getToken(HttpServletRequest request) {
        return getAuthHeader(request);
    }

}

SysUserService

@Slf4j
@Service
public class SysUserService implements UserDetailsService {

    @Autowired
    private SysUserMapper userMapper;

   @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException    {

        UserInfo userInfo = userMapper.getUserInfoByUsername(username);
        ParamAssert.notNull(userInfo, "用户不存在!");
        return userInfo;
    }

}

用户信息

@Data
@ApiModel("用户")
@EqualsAndHashCode(callSuper = true)
public class UserInfo implements UserDetails {

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

    @ApiModelProperty(notes = "姓名")
    private String name;

    @ApiModelProperty(notes = "编码")
    private String code;

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

    @ApiModelProperty(notes = "是否启用:true-启用,false-停用")
    private boolean enabled = true;
    
    private List<RoleAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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

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


测试

登录接口

启动项目,放开登录接口,登录

 返回结果

{
  "code": 200,
  "message": "SUCCESS",
  "data": {
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJMQy1NSU5GQU5HIiwic3ViIjoiYWRtaW4iLCJhdWQiOiJXZWIiLCJpYXQiOjE2NjUyMjAzODMsImV4cCI6MTY2NTQwMDM4M30.MhqQl79CgevBw2zeDuL2tsxgZaUe43e16-kw0aWMfCD5Hs9NI_D0dlwwvvr0znlORf6y5eyzyao8EqVIv09URQ"
  }
}

登陆失败提示

有关springboot 2.7整合spring security 5.7整合jwt实现用户登录注册与鉴权全记录的更多相关文章

  1. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  4. ruby - RVM "ERROR: Unable to checkout branch ."单用户 - 2

    我在新的Debian6VirtualBoxVM上安装RVM时遇到问题。我已经安装了所有需要的包并使用下载了安装脚本(curl-shttps://rvm.beginrescueend.com/install/rvm)>rvm,但以单个用户身份运行时bashrvm我收到以下错误消息:ERROR:Unabletocheckoutbranch.安装在这里停止,并且(据我所知)没有安装RVM的任何文件。如果我以root身份运行脚本(对于多用户安装),我会收到另一条消息:Successfullycheckedoutbranch''安装程序继续并指示成功,但未添加.rvm目录,甚至在修改我的.bas

  5. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 阿里云国际版免费试用:如何注册以及注意事项 - 2

    作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。​关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐

  9. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  10. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

随机推荐