Spring Boot整合实战Spring Security JWT权限鉴权系统

2022-12-07,,,,

目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以。像以前做项目的安全认证基于 session 的登录拦截,属于后端全栈式的开发的模式, 前后端分离鲜明的,前端不要接触过多的业务逻辑,都由后端解决, 服务端通过 JSON字符串,告诉前端用户有没有登录、认证,前端根据这些提示跳转对应的登录页、认证页等, 今天就Spring Boot整合Spring Security JWT实现登录认证以及权限认证,本文简单介绍用户和用户角色的权限问题

一. Spring Security简介

1.简介

一个能够为基于Spring的企业应用系统提供声明式的安全訪问控制解决方式的安全框架(简单说是对访问权限进行控制嘛),应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。 spring security的主要核心功能为 认证和授权,所有的架构也是基于这两个核心功能去实现的。

2.认证过程

用户使用用户名和密码进行登录。 Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。 AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。 通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。 上述介绍的就是 Spring Security 的认证过程。在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定。

二. JWT

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。具体的还是自行百度吧

三. 搭建系统

本系统使用技术栈

数据库: MySql

连接池: Hikari

持久层框架: MyBatis-plus

安全框架: Spring Security

安全传输工具: JWT

Json解析: fastjson

1.建数据库

设计用户和角色 设计一个最简角色表 role,包括 角色ID和 角色名称 role

    Create Table: CREATE TABLE `role` (

    `id` int(11) DEFAULT NULL,

    `name` char(10) DEFAULT NULL

    ) ENGINE=InnoDB DEFAULT CHARSET=utf8

设计一个最简用户表 user,包括 用户ID, 用户名, 密码 user

    Create Table: CREATE TABLE `user` (

    `id` int(11) DEFAULT NULL,

    `username` char(10) DEFAULT NULL,

    `password` char(100) DEFAULT NULL

    ) ENGINE=InnoDB DEFAULT CHARSET=utf8

关联表 user_role

    Create Table: CREATE TABLE `user_role` (

    `user_id` int(11) DEFAULT NULL,

    `role_id` int(11) DEFAULT NULL

    ) ENGINE=InnoDB DEFAULT CHARSET=utf8

2.新建Spring Boot工程

引入相关依赖

    <dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-security</artifactId>

    </dependency>

    <dependency>

    <groupId>org.springframework.security</groupId>

    <artifactId>spring-security-test</artifactId>

    <scope>test</scope>

    </dependency>

    <!--MySQL驱动-->

    <dependency>

    <groupId>mysql</groupId>

    <artifactId>mysql-connector-java</artifactId>

    <scope>runtime</scope>

    </dependency>

    <!--Mybatis-Plus-->

    <dependency>

    <groupId>com.baomidou</groupId>

    <artifactId>mybatis-plus</artifactId>

    <version>3.0.6</version>

    </dependency>

    <dependency>

    <groupId>com.baomidou</groupId>

    <artifactId>mybatis-plus-boot-starter</artifactId>

    <version>3.0.6</version>

    </dependency>

    <!-- 模板引擎 -->

    <dependency>

    <groupId>org.apache.velocity</groupId>

    <artifactId>velocity-engine-core</artifactId>

    <version>2.0</version>

    </dependency>

    <!--JWT-->

    <dependency>

    <groupId>io.jsonwebtoken</groupId>

    <artifactId>jjwt</artifactId>

    <version>0.9.0</version>

    </dependency>

    <!--lombok-->

    <dependency>

    <groupId>org.projectlombok</groupId>

    <artifactId>lombok</artifactId>

    <optional>true</optional>

    </dependency>

    <!--阿里fastjson-->

    <dependency>

    <groupId>com.alibaba</groupId>

    <artifactId>fastjson</artifactId>

    <version>1.2.4</version>

    </dependency>

配置文件

    # 数据源

    spring.datasource.driver-class-name=com.mysql.jdbc.Driver

    spring.datasource.url=jdbc:mysql://localhost:3306/spring_security?useUnicode=true&characterEncoding=utf-8

    spring.datasource.username=root

    spring.datasource.password=root

    #mybatis-plus配置

    #mapper对应文件

    mybatis-plus.mapper-locations=classpath:mapper/*.xml

    #实体扫描,多个package用逗号或者分号分隔

    mybatis-plus.typeAliasesPackage=com.li.springbootsecurity.model

    #执行的sql打印出来 开发/测试

    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

    #Hikari 连接池配置

    #最小空闲连接数量

    spring.datasource.hikari.minimum-idle=5

    #空闲连接存活最大时间,默认600000(10分钟)

    spring.datasource.hikari.idle-timeout=180000

    #连接池最大连接数,默认是10

    spring.datasource.hikari.maximum-pool-size=10

    #此属性控制从池返回的连接的默认自动提交行为,默认值:true

    spring.datasource.hikari.auto-commit=true

    #连接池名字

    spring.datasource.hikari.pool-name=HwHikariCP

    #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟

    spring.datasource.hikari.max-lifetime=1800000

    #数据库连接超时时间,默认30秒,即30000

    spring.datasource.hikari.connection-timeout=30000

    spring.datasource.hikari.connection-test-query=SELECT 1

    # JWT配置

    # 自定义 服务端根据secret生成token

    jwt.secret=mySecret

    # 头部

    jwt.header=Authorization

    # token有效时间

    jwt.expiration=604800

    # token头部

    jwt.tokenHead=Bearer

2.代码生成

这里简单说明下: 建表完成后 使用mybatis-plus代码生成(不了解的自行了解 后面会出教程 本文不做过多介绍)

生成代码

    package com.li.springbootsecurity.code;

    import com.baomidou.mybatisplus.annotation.DbType;

    import com.baomidou.mybatisplus.generator.AutoGenerator;

    import com.baomidou.mybatisplus.generator.config.DataSourceConfig;

    import com.baomidou.mybatisplus.generator.config.GlobalConfig;

    import com.baomidou.mybatisplus.generator.config.PackageConfig;

    import com.baomidou.mybatisplus.generator.config.StrategyConfig;

    import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

    /**

    * @Author 李号东

    * @Description mybatis-plus自动生成

    * @Date 08:07 2019-03-17

    * @Param

    * @return

    **/

    public class MyBatisPlusGenerator {

    public static void main(String[] args) {

    // 代码生成器

    AutoGenerator mpg = new AutoGenerator();

    //1. 全局配置

    GlobalConfig gc = new GlobalConfig();

    gc.setOutputDir("/Volumes/李浩东的移动硬盘/LiHaodong/springboot-security/src/main/java");

    gc.setOpen(false);

    gc.setFileOverride(true);

    gc.setBaseResultMap(true);//生成基本的resultMap

    gc.setBaseColumnList(false);//生成基本的SQL片段

    gc.setAuthor("lihaodong");// 作者

    mpg.setGlobalConfig(gc);

    //2. 数据源配置

    DataSourceConfig dsc = new DataSourceConfig();

    dsc.setDbType(DbType.MYSQL);

    dsc.setDriverName("com.mysql.jdbc.Driver");

    dsc.setUsername("root");

    dsc.setPassword("root");

    dsc.setUrl("jdbc:mysql://127.0.0.1:3306/test");

    mpg.setDataSource(dsc);

    //3. 策略配置globalConfiguration中

    StrategyConfig strategy = new StrategyConfig();

    strategy.setTablePrefix("");// 此处可以修改为您的表前缀

    strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略

    strategy.setSuperEntityClass("com.li.springbootsecurity.model");

    strategy.setInclude("role"); // 需要生成的表

    strategy.setEntityLombokModel(true);

    strategy.setRestControllerStyle(true);

    strategy.setControllerMappingHyphenStyle(true);

    mpg.setStrategy(strategy);

    //4. 包名策略配置

    PackageConfig pc = new PackageConfig();

    pc.setParent("com.li.springbootsecurity");

    pc.setEntity("model");

    mpg.setPackageInfo(pc);

    // 执行生成

    mpg.execute();

    }

    }

3.User类

简单的用户模型

    package com.li.springbootsecurity.model;

    import com.baomidou.mybatisplus.annotation.IdType;

    import com.baomidou.mybatisplus.annotation.TableId;

    import com.baomidou.mybatisplus.annotation.TableName;

    import com.baomidou.mybatisplus.extension.activerecord.Model;

    import lombok.*;

    import lombok.experimental.Accessors;

    import org.springframework.security.core.GrantedAuthority;

    import org.springframework.security.core.authority.SimpleGrantedAuthority;

    import org.springframework.security.core.userdetails.UserDetails;

    import java.util.ArrayList;

    import java.util.Collection;

    import java.util.List;

    /**

    * 用户类

    * @author lihaodong

    * @since 2019-03-14

    */

    @Setter

    @Getter

    @ToString

    @TableName("user")

    public class User extends Model<User>{

    private static final long serialVersionUID = 1L;

    private Integer id;

    private String username;

    private String password;

    }

4.Role类

    package com.li.springbootsecurity.model;

    import com.baomidou.mybatisplus.annotation.TableName;

    import com.baomidou.mybatisplus.extension.activerecord.Model;

    import lombok.*;

    import lombok.experimental.Accessors;

    /**

    * 角色类

    * @author lihaodong

    * @since 2019-03-14

    */

    @Setter

    @Getter

    @Builder

    @TableName("role")

    public class Role extends Model<User> {

    private static final long serialVersionUID = 1L;

    private Integer id;

    private String name;

    }

4.用户服务类

    package com.li.springbootsecurity.service;

    import com.li.springbootsecurity.bo.ResponseUserToken;

    import com.li.springbootsecurity.model.User;

    import com.baomidou.mybatisplus.extension.service.IService;

    import com.li.springbootsecurity.security.SecurityUser;

    /**

    * <p>

    * 用户服务类

    * </p>

    *

    * @author lihaodong

    * @since 2019-03-14

    */

    public interface IUserService extends IService<User> {

    /**

    * 通过用户名查找用户

    *

    * @param username 用户名

    * @return 用户信息

    */

    User findByUserName(String username);

    /**

    * 登陆

    * @param username

    * @param password

    * @return

    */

    ResponseUserToken login(String username, String password);

    /**

    * 根据Token获取用户信息

    * @param token

    * @return

    */

    SecurityUser getUserByToken(String token);

    }

5.安全用户模型 主要用来用户身份权限认证类 登陆身份认证

    package com.li.springbootsecurity.security;

    import com.li.springbootsecurity.model.Role;

    import com.li.springbootsecurity.model.User;

    import lombok.Getter;

    import lombok.Setter;

    import org.springframework.security.core.GrantedAuthority;

    import org.springframework.security.core.authority.SimpleGrantedAuthority;

    import org.springframework.security.core.userdetails.UserDetails;

    import java.util.ArrayList;

    import java.util.Collection;

    import java.util.Date;

    import java.util.List;

    /**

    * @Author 李号东

    * @Description 用户身份权限认证类 登陆身份认证

    * @Date 13:29 2019-03-16

    * @Param

    * @return

    **/

    @Setter

    @Getter

    public class SecurityUser extends User implements UserDetails {

    private static final long serialVersionUID = 1L;

    private Integer id;

    private String username;

    private String password;

    private Role role;

    private Date lastPasswordResetDate;

    public SecurityUser(Integer id, String username, Role role, String password) {

    this.id = id;

    this.username = username;

    this.password = password;

    this.role = role;

    }

    public SecurityUser(String username, String password, Role role) {

    this.username = username;

    this.password = password;

    this.role = role;

    }

    public SecurityUser(Integer id, String username, String password) {

    this.id = id;

    this.username = username;

    this.password = password;

    }

    //返回分配给用户的角色列表

    @Override

    public Collection<? extends GrantedAuthority> getAuthorities() {

    List<GrantedAuthority> authorities = new ArrayList<>();

    authorities.add(new SimpleGrantedAuthority(role.getName()));

    return authorities;

    }

    //账户是否未过期,过期无法验证

    @Override

    public boolean isAccountNonExpired() {

    return true;

    }

    //指定用户是否解锁,锁定的用户无法进行身份验证

    @Override

    public boolean isAccountNonLocked() {

    return true;

    }

    //指示是否已过期的用户的凭据(密码),过期的凭据防止认证

    @Override

    public boolean isCredentialsNonExpired() {

    return true;

    }

    //是否可用 ,禁用的用户不能身份验证

    @Override

    public boolean isEnabled() {

    return true;

    }

    }

此处所创建的 SecurityUser类继承了 Spring Security的 UserDetails接口,从而成为了一个符合 Security安全的用户,即通过继承 UserDetails,即可实现 Security中相关的安全功能。

6.创建JWT工具类

主要用于对 JWT Token进行各项操作,比如生成Token、验证Token、刷新Token等

    package com.li.springbootsecurity.utils;

    import com.alibaba.fastjson.JSON;

    import com.li.springbootsecurity.model.Role;

    import com.li.springbootsecurity.security.SecurityUser;

    import io.jsonwebtoken.CompressionCodecs;

    import org.springframework.beans.factory.annotation.Value;

    import org.springframework.security.core.GrantedAuthority;

    import org.springframework.security.core.userdetails.UserDetails;

    import org.springframework.stereotype.Component;

    import java.util.*;

    import java.util.concurrent.ConcurrentHashMap;

    import io.jsonwebtoken.Claims;

    import io.jsonwebtoken.Jwts;

    import io.jsonwebtoken.SignatureAlgorithm;

    /**

    * @Classname JwtTokenUtil

    * @Description JWT工具类

    * @Author 李号东 lihaodongmail@163.com

    * @Date 2019-03-14 14:54

    * @Version 1.0

    */

    @Component

    public class JwtTokenUtil {

    private static final String ROLE_REFRESH_TOKEN = "ROLE_REFRESH_TOKEN";

    private static final String CLAIM_KEY_USER_ID = "user_id";

    private static final String CLAIM_KEY_AUTHORITIES = "scope";

    private Map<String, String> tokenMap = new ConcurrentHashMap<>(32);

    /**

    * 密钥

    */

    @Value("${jwt.secret}")

    private String secret;

    /**

    * 有效期

    */

    @Value("${jwt.expiration}")

    private Long accessTokenExpiration;

    /**

    * 刷新有效期

    */

    @Value("${jwt.expiration}")

    private Long refreshTokenExpiration;

    private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;

    /**

    * 根据token 获取用户信息

    * @param token

    * @return

    */

    public SecurityUser getUserFromToken(String token) {

    SecurityUser userDetail;

    try {

    final Claims claims = getClaimsFromToken(token);

    int userId = getUserIdFromToken(token);

    String username = claims.getSubject();

    String roleName = claims.get(CLAIM_KEY_AUTHORITIES).toString();

    Role role = Role.builder().name(roleName).build();

    userDetail = new SecurityUser(userId, username, role, "");

    } catch (Exception e) {

    userDetail = null;

    }

    return userDetail;

    }

    /**

    * 根据token 获取用户ID

    * @param token

    * @return

    */

    private int getUserIdFromToken(String token) {

    int userId;

    try {

    final Claims claims = getClaimsFromToken(token);

    userId = Integer.parseInt(String.valueOf(claims.get(CLAIM_KEY_USER_ID)));

    } catch (Exception e) {

    userId = 0;

    }

    return userId;

    }

    /**

    * 根据token 获取用户名

    * @param token

    * @return

    */

    public String getUsernameFromToken(String token) {

    String username;

    try {

    final Claims claims = getClaimsFromToken(token);

    username = claims.getSubject();

    } catch (Exception e) {

    username = null;

    }

    return username;

    }

    /**

    * 根据token 获取生成时间

    * @param token

    * @return

    */

    public Date getCreatedDateFromToken(String token) {

    Date created;

    try {

    final Claims claims = getClaimsFromToken(token);

    created = claims.getIssuedAt();

    } catch (Exception e) {

    created = null;

    }

    return created;

    }

    /**

    * 生成令牌

    *

    * @param userDetail 用户

    * @return 令牌

    */

    public String generateAccessToken(SecurityUser userDetail) {

    Map<String, Object> claims = generateClaims(userDetail);

    claims.put(CLAIM_KEY_AUTHORITIES, authoritiesToArray(userDetail.getAuthorities()).get(0));

    return generateAccessToken(userDetail.getUsername(), claims);

    }

    /**

    * 根据token 获取过期时间

    * @param token

    * @return

    */

    private Date getExpirationDateFromToken(String token) {

    Date expiration;

    try {

    final Claims claims = getClaimsFromToken(token);

    expiration = claims.getExpiration();

    } catch (Exception e) {

    expiration = null;

    }

    return expiration;

    }

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {

    final Date created = getCreatedDateFromToken(token);

    return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)

    && (!isTokenExpired(token));

    }

    /**

    * 刷新令牌

    *

    * @param token 原令牌

    * @return 新令牌

    */

    public String refreshToken(String token) {

    String refreshedToken;

    try {

    final Claims claims = getClaimsFromToken(token);

    refreshedToken = generateAccessToken(claims.getSubject(), claims);

    } catch (Exception e) {

    refreshedToken = null;

    }

    return refreshedToken;

    }

    /**

    * 验证token 是否合法

    * @param token token

    * @param userDetails 用户信息

    * @return

    */

    public boolean validateToken(String token, UserDetails userDetails) {

    SecurityUser userDetail = (SecurityUser) userDetails;

    final long userId = getUserIdFromToken(token);

    final String username = getUsernameFromToken(token);

    return (userId == userDetail.getId()

    && username.equals(userDetail.getUsername())

    && !isTokenExpired(token)

    );

    }

    /**

    * 根据用户信息 重新获取token

    * @param userDetail

    * @return

    */

    public String generateRefreshToken(SecurityUser userDetail) {

    Map<String, Object> claims = generateClaims(userDetail);

    // 只授于更新 token 的权限

    String[] roles = new String[]{JwtTokenUtil.ROLE_REFRESH_TOKEN};

    claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(roles));

    return generateRefreshToken(userDetail.getUsername(), claims);

    }

    public void putToken(String userName, String token) {

    tokenMap.put(userName, token);

    }

    public void deleteToken(String userName) {

    tokenMap.remove(userName);

    }

    public boolean containToken(String userName, String token) {

    return userName != null && tokenMap.containsKey(userName) && tokenMap.get(userName).equals(token);

    }

    /***

    * 解析token 信息

    * @param token

    * @return

    */

    private Claims getClaimsFromToken(String token) {

    Claims claims;

    try {

    claims = Jwts.parser()

    .setSigningKey(secret)

    .parseClaimsJws(token)

    .getBody();

    } catch (Exception e) {

    claims = null;

    }

    return claims;

    }

    /**

    * 生成失效时间

    * @param expiration

    * @return

    */

    private Date generateExpirationDate(long expiration) {

    return new Date(System.currentTimeMillis() + expiration * 1000);

    }

    /**

    * 判断令牌是否过期

    *

    * @param token 令牌

    * @return 是否过期

    */

    private Boolean isTokenExpired(String token) {

    final Date expiration = getExpirationDateFromToken(token);

    return expiration.before(new Date());

    }

    /**

    * 生成时间是否在最后修改时间之前

    * @param created 生成时间

    * @param lastPasswordReset 最后修改密码时间

    * @return

    */

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {

    return (lastPasswordReset != null && created.before(lastPasswordReset));

    }

    private Map<String, Object> generateClaims(SecurityUser userDetail) {

    Map<String, Object> claims = new HashMap<>(16);

    claims.put(CLAIM_KEY_USER_ID, userDetail.getId());

    return claims;

    }

    /**

    * 生成token

    * @param subject 用户名

    * @param claims

    * @return

    */

    private String generateAccessToken(String subject, Map<String, Object> claims) {

    return generateToken(subject, claims, accessTokenExpiration);

    }

    private List authoritiesToArray(Collection<? extends GrantedAuthority> authorities) {

    List<String> list = new ArrayList<>();

    for (GrantedAuthority ga : authorities) {

    list.add(ga.getAuthority());

    }

    return list;

    }

    private String generateRefreshToken(String subject, Map<String, Object> claims) {

    return generateToken(subject, claims, refreshTokenExpiration);

    }

    /**

    * 生成token

    * @param subject 用户名

    * @param claims

    * @param expiration 过期时间

    * @return

    */

    private String generateToken(String subject, Map<String, Object> claims, long expiration) {

    return Jwts.builder()

    .setClaims(claims)

    .setSubject(subject)

    .setId(UUID.randomUUID().toString())

    .setIssuedAt(new Date())

    .setExpiration(generateExpirationDate(expiration))

    .compressWith(CompressionCodecs.DEFLATE)

    .signWith(SIGNATURE_ALGORITHM, secret)

    .compact();

    }

    }

7.创建Token过滤器,用于每次外部对接口请求时的Token处理

    package com.li.springbootsecurity.config;

    import com.li.springbootsecurity.security.SecurityUser;

    import com.li.springbootsecurity.utils.JwtTokenUtil;

    import lombok.extern.slf4j.Slf4j;

    import org.apache.commons.lang3.StringUtils;

    import org.springframework.beans.factory.annotation.Value;

    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

    import org.springframework.security.core.context.SecurityContextHolder;

    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

    import org.springframework.stereotype.Component;

    import org.springframework.web.filter.OncePerRequestFilter;

    import javax.annotation.Resource;

    import javax.servlet.FilterChain;

    import javax.servlet.ServletException;

    import javax.servlet.http.HttpServletRequest;

    import javax.servlet.http.HttpServletResponse;

    import java.io.IOException;

    import java.util.Date;

    /**

    * @Author 李号东

    * @Description token过滤器来验证token有效性 引用的stackoverflow一个答案里的处理方式

    * @Date 00:32 2019-03-17

    * @Param

    * @return

    **/

    @Slf4j

    @Component

    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Value("${jwt.header}")

    private String tokenHeader;

    @Value("${jwt.tokenHead}")

    private String authTokenStart;

    @Resource

    private JwtTokenUtil jwtTokenUtil;

    @Override

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

    String authToken = request.getHeader(this.tokenHeader);

    System.out.println(authToken);

    if (StringUtils.isNotEmpty(authToken) && authToken.startsWith(authTokenStart)) {

    authToken = authToken.substring(authTokenStart.length());

    log.info("请求" + request.getRequestURI() + "携带的token值:" + authToken);

    //如果在token过期之前触发接口,我们更新token过期时间,token值不变只更新过期时间

    //获取token生成时间

    Date createTokenDate = jwtTokenUtil.getCreatedDateFromToken(authToken);

    log.info("createTokenDate: " + createTokenDate);

    } else {

    // 不按规范,不允许通过验证

    authToken = null;

    }

    String username = jwtTokenUtil.getUsernameFromToken(authToken);

    log.info("JwtAuthenticationTokenFilter[doFilterInternal] checking authentication " + username);

    if (jwtTokenUtil.containToken(username, authToken) && username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

    SecurityUser userDetail = jwtTokenUtil.getUserFromToken(authToken);

    if (jwtTokenUtil.validateToken(authToken, userDetail)) {

    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities());

    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

    log.info(String.format("Authenticated userDetail %s, setting security context", username));

    SecurityContextHolder.getContext().setAuthentication(authentication);

    }

    }

    chain.doFilter(request, response);

    }

    }

8.创建RestAuthenticationAccessDeniedHandler 自定义权限不足处理类

    package com.li.springbootsecurity.config;

    import com.li.springbootsecurity.bo.ResultCode;

    import com.li.springbootsecurity.bo.ResultJson;

    import com.li.springbootsecurity.bo.ResultUtil;

    import lombok.extern.slf4j.Slf4j;

    import org.springframework.security.access.AccessDeniedException;

    import org.springframework.security.web.access.AccessDeniedHandler;

    import org.springframework.stereotype.Component;

    import javax.servlet.http.HttpServletRequest;

    import javax.servlet.http.HttpServletResponse;

    import java.io.IOException;

    import java.io.PrintWriter;

    /**

    * @Author 李号东

    * @Description 权限不足处理类 返回403

    * @Date 00:31 2019-03-17

    * @Param

    * @return

    **/

    @Slf4j

    @Component("RestAuthenticationAccessDeniedHandler")

    public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler {

    @Override

    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException {

    StringBuilder msg = new StringBuilder("请求: ");

    msg.append(httpServletRequest.getRequestURI()).append(" 权限不足,无法访问系统资源.");

    log.info(msg.toString());

    ResultUtil.writeJavaScript(httpServletResponse, ResultCode.FORBIDDEN, msg.toString());

    }

    }

9.创建JwtAuthenticationEntryPoint 认证失败处理类

    package com.li.springbootsecurity.config;

    import com.li.springbootsecurity.bo.ResultCode;

    import com.li.springbootsecurity.bo.ResultUtil;

    import lombok.extern.slf4j.Slf4j;

    import org.springframework.security.authentication.BadCredentialsException;

    import org.springframework.security.authentication.InsufficientAuthenticationException;

    import org.springframework.security.core.AuthenticationException;

    import org.springframework.security.web.AuthenticationEntryPoint;

    import org.springframework.stereotype.Component;

    import javax.servlet.http.HttpServletRequest;

    import javax.servlet.http.HttpServletResponse;

    import java.io.IOException;

    import java.io.Serializable;

    /**

    * @Author 李号东

    * @Description 认证失败处理类 返回401

    * @Date 00:32 2019-03-17

    * @Param

    * @return

    **/

    @Slf4j

    @Component

    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

    private static final long serialVersionUID = -8970718410437077606L;

    @Override

    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {

    StringBuilder msg = new StringBuilder("请求访问: ");

    msg.append(httpServletRequest.getRequestURI()).append(" 接口, 经jwt 认证失败,无法访问系统资源.");

    log.info(msg.toString());

    log.info(e.toString());

    // 用户登录时身份认证未通过

    if (e instanceof BadCredentialsException) {

    log.info("用户登录时身份认证失败.");

    ResultUtil.writeJavaScript(httpServletResponse, ResultCode.UNAUTHORIZED, msg.toString());

    } else if (e instanceof InsufficientAuthenticationException) {

    log.info("缺少请求头参数,Authorization传递是token值所以参数是必须的.");

    ResultUtil.writeJavaScript(httpServletResponse, ResultCode.NO_TOKEN, msg.toString());

    } else {

    log.info("用户token无效.");

    ResultUtil.writeJavaScript(httpServletResponse, ResultCode.TOKEN_INVALID, msg.toString());

    }

    }

    }

10.Spring Security web安全配置类编写 可以说是重中之重

这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure配置,来实现用户自定义的部分

    package com.li.springbootsecurity.config;

    import com.li.springbootsecurity.model.Role;

    import com.li.springbootsecurity.model.User;

    import com.li.springbootsecurity.security.SecurityUser;

    import com.li.springbootsecurity.service.IRoleService;

    import com.li.springbootsecurity.service.IUserService;

    import lombok.extern.slf4j.Slf4j;

    import org.springframework.beans.factory.annotation.Autowired;

    import org.springframework.beans.factory.annotation.Qualifier;

    import org.springframework.context.annotation.Bean;

    import org.springframework.context.annotation.Configuration;

    import org.springframework.security.authentication.AuthenticationManager;

    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

    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.EnableWebSecurity;

    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

    import org.springframework.security.config.http.SessionCreationPolicy;

    import org.springframework.security.core.userdetails.UserDetails;

    import org.springframework.security.core.userdetails.UserDetailsService;

    import org.springframework.security.core.userdetails.UsernameNotFoundException;

    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

    import org.springframework.security.web.access.AccessDeniedHandler;

    import org.springframework.security.web.authentication.AuthenticationFailureHandler;

    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

    import org.springframework.security.web.util.matcher.RequestMatcher;

    /**

    * @Author 李号东

    * @Description Security配置类

    * @Date 00:36 2019-03-17

    * @Param

    * @return

    **/

    @Slf4j

    @Configuration

    @EnableWebSecurity //启动web安全性

    //@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法级的权限注解 性设置后控制器层的方法前的@PreAuthorize("hasRole('admin')") 注解才能起效

    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired

    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Autowired

    private AccessDeniedHandler accessDeniedHandler;

    @Autowired

    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**

    * 解决 无法直接注入 AuthenticationManager

    * @return

    * @throws Exception

    */

    @Bean

    @Override

    public AuthenticationManager authenticationManagerBean() throws Exception {

    return super.authenticationManagerBean();

    }

    @Autowired

    public WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler,

    @Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler,

    JwtAuthenticationTokenFilter authenticationTokenFilter) {

    this.unauthorizedHandler = unauthorizedHandler;

    this.accessDeniedHandler = accessDeniedHandler;

    this.authenticationTokenFilter = authenticationTokenFilter;

    }

    /**

    * 配置策略

    *

    * @param httpSecurity

    * @throws Exception

    */

    @Override

    protected void configure(HttpSecurity httpSecurity) throws Exception {

    httpSecurity

    // 由于使用的是JWT,我们这里不需要csrf

    .csrf().disable()

    // 权限不足处理类

    .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()

    // 认证失败处理类

    .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

    // 基于token,所以不需要session

    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

    .authorizeRequests()

    // 对于登录login要允许匿名访问

    .antMatchers("/login","/favicon.ico").permitAll()

    // 需要拥有admin权限

    .antMatchers("/user").hasAuthority("admin")

    // 除上面外的所有请求全部需要鉴权认证

    .anyRequest().authenticated();

    // 禁用缓存

    httpSecurity.headers().cacheControl();

    // 添加JWT filter

    httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

    }

    @Autowired

    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

    auth

    // 设置UserDetailsService

    .userDetailsService(userDetailsService())

    // 使用BCrypt进行密码的hash

    .passwordEncoder(passwordEncoder());

    auth.eraseCredentials(false);

    }

    /**

    * 装载BCrypt密码编码器 密码加密

    *

    * @return

    */

    @Bean

    public BCryptPasswordEncoder passwordEncoder() {

    return new BCryptPasswordEncoder();

    }

    /**

    * 登陆身份认证

    *

    * @return

    */

    @Override

    @Bean

    public UserDetailsService userDetailsService() {

    return new UserDetailsService() {

    @Autowired

    private IUserService userService;

    @Autowired

    private IRoleService roleService;

    @Override

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

    log.info("登录用户:" + username);

    User user = userService.findByUserName(username);

    if (user == null) {

    log.info("登录用户:" + username + " 不存在.");

    throw new UsernameNotFoundException("登录用户:" + username + " 不存在");

    }

    //获取用户拥有的角色

    Role role = roleService.findRoleByUserId(user.getId());

    return new SecurityUser(username, user.getPassword(), role);

    }

    };

    }

    }

11.创建测试的 LoginController:

    package com.li.springbootsecurity.controller;

    import com.li.springbootsecurity.bo.ResponseUseroken;

    import com.li.springbootsecurity.bo.ResultCode;

    import com.li.springbootsecurity.bo.ResultJson;

    import com.li.springbootsecurity.model.User;

    import com.li.springbootsecurity.security.SecurityUser;

    import com.li.springbootsecurity.service.IUserService;

    import org.springframework.beans.factory.annotation.Autowired;

    import org.springframework.beans.factory.annotation.Value;

    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

    import org.springframework.stereotype.Controller;

    import org.springframework.web.bind.annotation.*;

    import javax.servlet.http.HttpServletRequest;

    /**

    * @Classname LoginController

    * @Description 测试

    * @Author 李号东 lihaodongmail@163.com

    * @Date 2019-03-16 10:06

    * @Version 1.0

    */

    @Controller

    public class LoginController {

    @Autowired

    private IUserService userService;

    @Value("${jwt.header}")

    private String tokenHeader;

    /**

    * @Author 李号东

    * @Description 登录

    * @Date 10:18 2019-03-17

    * @Param [user]

    * @return com.li.springbootsecurity.bo.ResultJson<com.li.springbootsecurity.bo.ResponseUserToken>

    **/

    @RequestMapping(value = "/login")

    @ResponseBody

    public ResultJson<ResponseUserToken> login(User user) {

    System.out.println(user);

    ResponseUserToken response = userService.login(user.getUsername(), user.getPassword());

    return ResultJson.ok(response);

    }

    /**

    * @Author 李号东

    * @Description 获取用户信息 在WebSecurityConfig配置只有admin权限才可以访问 主要用来测试权限

    * @Date 10:17 2019-03-17

    * @Param [request]

    * @return com.li.springbootsecurity.bo.ResultJson

    **/

    @GetMapping(value = "/user")

    @ResponseBody

    public ResultJson getUser(HttpServletRequest request) {

    String token = request.getHeader(tokenHeader);

    if (token == null) {

    return ResultJson.failure(ResultCode.UNAUTHORIZED);

    }

    SecurityUser securityUser = userService.getUserByToken(token);

    return ResultJson.ok(securityUser);

    }

    public static void main(String[] args) {

    String password = "admin";

    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(4);

    String enPassword = encoder.encode(password);

    System.out.println(enPassword);

    }

    }

接下来启动工程,实验测试看看效果

权限测试 访问/use 接口 由于test用户角色是普通用户没有权限去访问

测试说明

1. 数据库数据

数据库已经新建两个用户 一个test 一个admin 密码都是admin

角色 一个 admin管理员 一个genreal普通用户

user_role进行关联

2. 管理员登录测试

接下来进行用户登录,并获得后台向用户颁发的JWT Token

权限测试

(1) 不带token访问接口

(2) 带token访问

3. 普通用户登录

权限测试 访问/use 接口 由于test用户角色是普通用户没有权限去访问

经过一系列的测试过程, 最后还是很满意的 前后端分离的权限系统设计就这样做好了

不管是什么架构 涉及到安全问题总会比其他框架更难一点

后面会进行优化 以及进行集成微服务oauth 2.0 敬请期待吧

本文涉及的东西还是很多的 有的不好理解 建议大家去GitHUb获取源码进行分析

源码下载: https://github.com/LiHaodong888/SpringBootLearn

Spring Boot整合实战Spring Security JWT权限鉴权系统的相关教程结束。

《Spring Boot整合实战Spring Security JWT权限鉴权系统.doc》

下载本文的Word格式文档,以方便收藏与打印。