目录

前言

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

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,有些对系统资源操作比如删除、添加、更新只能特定人才具有。

过滤器链

springboot es 配置账号密码 springboot用户登录注册_后端

核心组件

AuthenticationManager

SecurityContextHolder

PasswordEncoder

UserDetails

UserDetailsService

BasicAuthenticationFilter

AuthenticationEntryPoint

登录流程图

springboot es 配置账号密码 springboot用户登录注册_java_02

集成流程

集成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;
    }
}

测试

登录接口

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

springboot es 配置账号密码 springboot用户登录注册_字符串_03

登录service代码

主要是验证用户名密码的正确性,如果正确,生成一个token信息并返回

public AuthResponseVO login(AuthRequestVO requestVO) {

        log.debug("User '{}' login", requestVO.getUsername());

        String username = requestVO.getUsername();
        String password = requestVO.getPassword();

        authenticate(username, password);

        String token = jwtService.generateToken(username);

        AuthResponseVO responseVO = new AuthResponseVO();
        responseVO.setToken(token);
        return responseVO;
    }

    private void authenticate(String username, String password) {
        try {
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(username, password));
            UserInfo userInfo = (UserInfo) authentication.getPrincipal();
            log.info("User '{}' login", userInfo);
        } catch (Exception e) {
            log.error("User '{}' login error {} ", username, e.getMessage(), e);
            throw new AuthTokenException(e.getMessage());
        }
    }

 返回结果

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

登陆失败提示

springboot es 配置账号密码 springboot用户登录注册_spring_04