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

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

一. Spring Security简介

1. 简介

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

  1. 用户使用用户名和密码进行登录。
  2. Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
  3. 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
  4. AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
  5. 通过调用 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

Create Table: CREATE TABLE `role` (
  `id` int(11) DEFAULT NULL,
  `name` char(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8

 

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>

 

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

6. 创建JWT工具类

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安全配置类编写 可以说是重中之重

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.ResponseUserToken;
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);
    }
}

 

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

测试说明

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

有问题可以添加我的公众号

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值