SpringBoot+Security+JWT基础

title: SpringBoot+Security+JWT基础
date: 2019-07-04
author: maxzhao
tags:
  - JAVA
  - SpringBoot
  - Security
  - JWT
categories:
  - SpringBoot
  - Security

First

  • 我第一次使用,所以代码中的注释和说明比较多

优点

  • 由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

缺点

Security 基本原理

下面每个类或接口的作用,之后都会有代码.


了解 Token结构

Token是一个很长的字符串,中间用点(.)分隔成三个部分。

JWT 的三个部分依次如下。

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

Header(头部)

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

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

Payload(负载)

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

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

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

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

Signature(签名)

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

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

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

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

思路

  1. 构建
  2. 导入 security 、 jwt 依赖
  3. 用户的验证(service 、 dao 、model)
  4. 实现UserDetailsServiceUserDetails接口
  5. 可选:实现PasswordEncoder 接口(密码加密)
  6. 验证用户登录信息、用户权限的拦截器
  7. security 配置
  8. 登录认证 API

构建

1. 构建

创建个项目

2.导入 security 、 jwt 依赖

<spring-security-jwt.version>1.0.9.RELEASE</spring-security-jwt.version>
<jjwt.version>0.9.1</jjwt.version>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jjwt.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>${spring-security-jwt.version}</version>
    <scope>compile</scope>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.16</version>
</dependency>
<!--使用啦Lombok插件,需要自己添加 其它需要自己添加了-->

3.用户的验证(service 、 dao 、model)

model

/**
 * 用户表
 *
 *
 * @author maxzhao
 * @date 2019-6-6 13:53:17
 */
@Accessors(chain = true)
@Data
@Entity
@Table(name = "app_user", schema = "", catalog = "")
@ApiModel(value = "用户表", description = "用户表")
public class AppUser implements Serializable {
    private static final long serialVersionUID = -1L;

    @Id
    @Column(name = "ID",unique = true)
    private Long id;

    @Basic
    @Column(name = "LIVE_ADDRESS")
    private String liveAddress;

    @Basic
    @Column(name = "LOGIN_NAME")
    private String loginName;
    @Basic
    @Column(name = "PASSWORD")
    private String password;
/* 省略 其它*/
}

dao

/**
 * 用户表
 * Repository
 *
 * @author maxzhao
 * @date 2019-5-21 11:17:39
 */
@Repository(value = "appUserRepository")
public interface AppUserRepository extends JpaRepository<AppUser, Long>, JpaSpecificationExecutor<AppUser> {
    /**
     * 根据登录名 查询当前用户
     *
     * @param loginName 登录名
     * @return
     * @author maxzhao
     * @date 2019-05-22
     */
    List<AppUser> findByLoginNameEquals(String loginName);

}

service


/**
 * 用户表
 * Service
 *
 * @author maxzhao
 * @date 2019-5-21 11:17:39
 */
public interface AppUserService {
    /**
     * 保存
     *
     * @param appUser
     * @return
     * @author maxzhao
     * @date 2019-06-19
     */
    AppUser saveOne(AppUser appUser);

    /**
     * 根据登录名查询 当前登录用户
     *
     * @param loginName
     * @return
     */
    AppUser findByLoginName(String loginName);
}
/**
 * 用户表
 * ServiceImpl
 *
 * @author maxzhao
 * @date 2019-5-21 11:17:39
 */
@Service(value = "appUserService")
public class AppUserServiceImpl implements AppUserService {

    @Resource(name = "appUserRepository")
    private AppUserRepository appUserRepository;

    @Override
    public AppUser saveOne(AppUser appUser) {
        return appUserRepository.save(appUser);
    }

    @Override
    public AppUser findByLoginName(String loginName) {
        List<AppUser> appUserList = appUserRepository.findByLoginNameEquals(loginName);
        return appUserList.size() > 0 ? appUserList.get(0) : null;
    }
}

Jwt 工具类

/**
 * <p>jjwt封装一下方便调用</p>
 * <p>JwtTokenUtil</p>
 *
 * @author maxzhao
 * @date 2019-07-04 13:30
 */
public class JwtTokenUtil {

    /**
     * 密钥
     */
    private static final String SECRET = "jwt_secret_gtboot";
    private static final String ISS = "gtboot";

    /**
     * 过期时间是 1800 秒
     */
    private static final long EXPIRATION = 1800L;

    public static String createToken(String issuer, String subject, long expiration) {
        return createToken(issuer, subject, expiration, null);
    }

    /**
     * 创建 token
     *
     * @param issuer     签发人
     * @param subject    主体,即用户信息的JSON
     * @param expiration 有效时间(秒)
     * @param claims     自定义参数
     * @return
     * @description todo https://www.cnblogs.com/wangshouchang/p/9551748.html
     */
    public static String createToken(String issuer, String subject, long expiration, Claims claims) {
        return Jwts.builder()
                // JWT_ID:是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
//                .setId(id)
                // 签名算法以及密匙
                .signWith(SignatureAlgorithm.HS512, SECRET)
                // 自定义属性
                .setClaims(null)
                // 主题:代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .setSubject(subject)
                // 受众
//                .setAudience(loginName)
                // 签发人
                .setIssuer(Optional.ofNullable(issuer).orElse(ISS))
                // 签发时间
                .setIssuedAt(new Date())
                // 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + (expiration > 0 ? expiration : EXPIRATION) * 1000))
                .compact();
    }

    /**
     * 从 token 中获取主题信息
     *
     * @param token
     * @return
     */
    public static String getProperties(String token) {
        return getTokenBody(token).getSubject();
    }

    /**
     * 校验是否过期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        return getTokenBody(token).getExpiration().before(new Date());
    }

    /**
     * 获得 token 的 body
     *
     * @param token
     * @return
     */
    private static Claims getTokenBody(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

4.实现UserDetailsServiceUserDetails接口

UserDetailsService

可以把角色相关禁用掉,然后修改参数.

我也会把角色相关操作,以及表的 sql 放到最后.

/**
 * 加载特定于用户的数据的核心接口。
 * 它作为用户DAO在整个框架中使用,是DaoAuthenticationProvider使用的策略。
 * 该接口只需要一个只读方法,这简化了对新数据访问策略的支持。
 *
 * @author maxzhao
 */
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
    /**
     * 用户操作服务
     */
    @Resource(name = "appUserService")
    private AppUserService appUserService;

    /**
     * 用户角色服务
     */
    @Resource(name = "appRoleService")
    private AppRoleService appRoleService;

    /**
     * 根据用户登录名定位用户。
     *
     * @param loginName
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {

        UserDetails userDetails = null;
        try {
            AppUser appUser = appUserService.findByLoginName(loginName);
            if (appUser != null) {
                // 查询当前用户的权限
                List<AppRole> appRoleList = appRoleService.findByUserId(appUser.getId());
                Collection<GrantedAuthority> authorities = new ArrayList<>();
                for (AppRole appRole : appRoleList) {
                    SimpleGrantedAuthority grant = new SimpleGrantedAuthority(appRole.getConstName());
                    authorities.add(grant);
                }
                //封装自定义UserDetails类
                userDetails = new UserDetailsImpl(appUser, authorities);
            } else {
                throw new UsernameNotFoundException("该用户不存在!");
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        return userDetails;
    }

}

UserDetails

/**
 * 自定义用户身份信息
 * 提供核心用户信息。
 * 出于安全目的,Spring Security不直接使用实现。它们只是存储用户信息,这些信息稍后封装到身份验证对象中。这允许将非安全相关的用户信息(如电子邮件地址、电话号码等)存储在一个方便的位置。
 * 具体实现必须特别注意,以确保每个方法的非空契约都得到了执行。有关参考实现(您可能希望在代码中对其进行扩展或使用),请参见User。
 *
 * @author maxzhao
 * @date 2019-05-22
 */
public class UserDetailsImpl implements UserDetails {
    private static final long serialVersionUID = 1L;
    /**
     * 用户信息
     */
    private AppUser appUser;
    /**
     * 用户角色
     */
    private Collection<? extends GrantedAuthority> authorities;

    public UserDetailsImpl(AppUser appUser, Collection<? extends GrantedAuthority> authorities) {
        super();
        this.appUser = appUser;
        this.authorities = authorities;
    }

    /**
     * 返回用户所有角色的封装,一个Role对应一个GrantedAuthority
     *
     * @return 返回授予用户的权限。
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    /*    Collection<GrantedAuthority> authorities = new ArrayList<>();
        String username = this.getUsername();
        if (username != null) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username);
            authorities.add(authority);
        }*/
        return authorities;
    }
    /**
     * 返回用于验证用户身份的密码。
     *
     * @return Returns the password used to authenticate the user.
     */
    @Override
    public String getPassword() {
        return appUser.getPassword();
    }
    /**
     * @return
     */
    @Override
    public String getUsername() {
        return appUser.getLoginName();
    }
    /**
     * 判断账号是否已经过期,默认没有过期
     *
     * @return true 没有过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return appUser.getExpiration() == null || appUser.getExpiration().before(new Date());
    }
    /**
     * 判断账号是否被锁定,默认没有锁定
     *
     * @return true 没有锁定  false 锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return appUser.getLockStatus() == null || appUser.getLockStatus() == 0;
    }
    /**
     * todo 判断信用凭证是否过期,默认没有过期
     *
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    /**
     * 判断账号是否可用,默认可用
     *
     * @return
     */
    @Override
    public boolean isEnabled() {
        return appUser.getDelStatus() == 0;
    }
}

5.可选:实现PasswordEncoder 接口(密码加密)

/**
 * PasswordEncoderImpl
 *
 * @author maxzhao
 * @date 2019-05-23 15:55
 */
@Service("passwordEncoder")
public class PasswordEncoderImpl implements PasswordEncoder {
    private final int strength;
    private final SecureRandom random;
    private Pattern BCRYPT_PATTERN;
    private Logger logger;

    /**
     * 构造函数用于设置不同的加密过程
     */
    public PasswordEncoderImpl() {
        this(-1);
    }

    public PasswordEncoderImpl(int strength) {
        this(strength, null);
    }

    public PasswordEncoderImpl(int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
        this.logger = LoggerFactory.getLogger(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.strength = strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }

    /**
     * 对原始密码进行编码。通常,一个好的编码算法应用SHA-1或更大的哈希值和一个8字节或更大的随机生成的salt。
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or greater hash combined with an 8-byte or greater randomly generated salt.
     *
     * @param rawPassword
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        String salt;
        if (this.strength > 0) {
            if (this.random != null) {
                salt = BCrypt.gensalt(this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.strength);
            }
        } else {
            salt = BCrypt.gensalt();
        }

        return BCrypt.hashpw(rawPassword.toString(), salt);
    }

    /**
     * 验证从存储中获得的已编码密码在经过编码后是否与提交的原始密码匹配。
     * 如果密码匹配,返回true;如果密码不匹配,返回false。存储的密码本身永远不会被解码。
     *
     * @param rawPassword     the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

    /**
     * 如果为了更好的安全性,应该再次对已编码的密码进行编码,则返回true,否则为false。
     *
     * @param encodedPassword the encoded password to check
     * @return Returns true if the encoded password should be encoded again for better security, else false. The default implementation always returns false.
     */
    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

6.验证用户登录信息、用户权限的拦截器

  • JwtAuthenticationFilter用户账号的验证
  • JwtAuthorizationFilter用户权限的验证

JwtAuthenticationFilter继承于UsernamePasswordAuthenticationFilter
该拦截器用于获取用户登录的信息,只需创建一个 token并调用 authenticationManager.authenticate()spring-security去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给spring去操作。

这个操作有点像是shirosubject.login(new UsernamePasswordToken()),验证的事情交给框架。

7.security 配置

8.登录认证 API

配置文件

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/maxzhao_ittest?charset=utf8mb4&useSSL=false
    username: maxzhao
    password: maxzhao
  main:
    allow-bean-definition-overriding: true

  jpa:
    database: MYSQL
    database-plinatform: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
    generate-ddl: true
    open-in-view: false

    hibernate:
      ddl-auto: update
    #       naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
    properties:
      #不加此配置,获取不到当前currentsession
      hibernate:
        current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
        dialect: org.hibernate.dialect.MySQL5Dialect
# 多数据源配置
gt:
  maxzhao:
    boot:
    #主动开启多数据源
      multiDatasourceOpen: true
      datasource[0]:
        dbName: second
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/pos?charset=utf8mb4&useSSL=false
        username: maxzhao
        password: maxzhao
      datasource[1]:
        dbName: third
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/biz?charset=utf8mb4&useSSL=false
        username: maxzhao
        password: maxzhao

本文地址:
SpringBoot+Security+JWT基础

gitee

推荐
SpringBoot+Security+JWT基础
SpringBoot+Security+JWT进阶:一、自定义认证
SpringBoot+Security+JWT进阶:二、自定义认证实践

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269