目前流行的前后端分离让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

  1. Create Table: CREATE TABLE `role` (

  2. `id` int(11) DEFAULT NULL,

  3. `name` char(10) DEFAULT NULL

  4. ) ENGINE=InnoDB DEFAULT CHARSET=utf8

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

  1. Create Table: CREATE TABLE `user` (

  2. `id` int(11) DEFAULT NULL,

  3. `username` char(10) DEFAULT NULL,

  4. `password` char(100) DEFAULT NULL

  5. ) ENGINE=InnoDB DEFAULT CHARSET=utf8

关联表 user_role

  1. Create Table: CREATE TABLE `user_role` (

  2. `user_id` int(11) DEFAULT NULL,

  3. `role_id` int(11) DEFAULT NULL

  4. ) ENGINE=InnoDB DEFAULT CHARSET=utf8

2.新建Spring Boot工程

引入相关依赖

  1. <dependency>

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

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

  4. </dependency>

  5. <dependency>

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

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

  8. <scope>test</scope>

  9. </dependency>

  10. <!--MySQL驱动-->

  11. <dependency>

  12. <groupId>mysql</groupId>

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

  14. <scope>runtime</scope>

  15. </dependency>

  16. <!--Mybatis-Plus-->

  17. <dependency>

  18. <groupId>com.baomidou</groupId>

  19. <artifactId>mybatis-plus</artifactId>

  20. <version>3.0.6</version>

  21. </dependency>

  22. <dependency>

  23. <groupId>com.baomidou</groupId>

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

  25. <version>3.0.6</version>

  26. </dependency>

  27. <!-- 模板引擎 -->

  28. <dependency>

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

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

  31. <version>2.0</version>

  32. </dependency>

  33. <!--JWT-->

  34. <dependency>

  35. <groupId>io.jsonwebtoken</groupId>

  36. <artifactId>jjwt</artifactId>

  37. <version>0.9.0</version>

  38. </dependency>

  39. <!--lombok-->

  40. <dependency>

  41. <groupId>org.projectlombok</groupId>

  42. <artifactId>lombok</artifactId>

  43. <optional>true</optional>

  44. </dependency>

  45. <!--阿里fastjson-->

  46. <dependency>

  47. <groupId>com.alibaba</groupId>

  48. <artifactId>fastjson</artifactId>

  49. <version>1.2.4</version>

  50. </dependency>

配置文件

  1. # 数据源

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

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

  4. spring.datasource.username=root

  5. spring.datasource.password=root

  6. #mybatis-plus配置

  7. #mapper对应文件

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

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

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

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

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

  13. #Hikari 连接池配置

  14. #最小空闲连接数量

  15. spring.datasource.hikari.minimum-idle=5

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

  17. spring.datasource.hikari.idle-timeout=180000

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

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

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

  21. spring.datasource.hikari.auto-commit=true

  22. #连接池名字

  23. spring.datasource.hikari.pool-name=HwHikariCP

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

  25. spring.datasource.hikari.max-lifetime=1800000

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

  27. spring.datasource.hikari.connection-timeout=30000

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

  29. # JWT配置

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

  31. jwt.secret=mySecret

  32. # 头部

  33. jwt.header=Authorization

  34. # token有效时间

  35. jwt.expiration=604800

  36. # token头部

  37. jwt.tokenHead=Bearer

2.代码生成

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

生成代码

  1. package com.li.springbootsecurity.code;

  2. import com.baomidou.mybatisplus.annotation.DbType;

  3. import com.baomidou.mybatisplus.generator.AutoGenerator;

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

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

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

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

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

  9. /**

  10. * @Author 李号东

  11. * @Description mybatis-plus自动生成

  12. * @Date 08:07 2019-03-17

  13. * @Param

  14. * @return

  15. **/

  16. public class MyBatisPlusGenerator {

  17. public static void main(String[] args) {

  18. // 代码生成器

  19. AutoGenerator mpg = new AutoGenerator();

  20. //1. 全局配置

  21. GlobalConfig gc = new GlobalConfig();

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

  23. gc.setOpen(false);

  24. gc.setFileOverride(true);

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

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

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

  28. mpg.setGlobalConfig(gc);

  29. //2. 数据源配置

  30. DataSourceConfig dsc = new DataSourceConfig();

  31. dsc.setDbType(DbType.MYSQL);

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

  33. dsc.setUsername("root");

  34. dsc.setPassword("root");

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

  36. mpg.setDataSource(dsc);

  37. //3. 策略配置globalConfiguration中

  38. StrategyConfig strategy = new StrategyConfig();

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

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

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

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

  43. strategy.setEntityLombokModel(true);

  44. strategy.setRestControllerStyle(true);

  45. strategy.setControllerMappingHyphenStyle(true);

  46. mpg.setStrategy(strategy);

  47. //4. 包名策略配置

  48. PackageConfig pc = new PackageConfig();

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

  50. pc.setEntity("model");

  51. mpg.setPackageInfo(pc);

  52. // 执行生成

  53. mpg.execute();

  54. }

  55. }

3.User类

简单的用户模型

  1. package com.li.springbootsecurity.model;

  2. import com.baomidou.mybatisplus.annotation.IdType;

  3. import com.baomidou.mybatisplus.annotation.TableId;

  4. import com.baomidou.mybatisplus.annotation.TableName;

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

  6. import lombok.*;

  7. import lombok.experimental.Accessors;

  8. import org.springframework.security.core.GrantedAuthority;

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

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

  11. import java.util.ArrayList;

  12. import java.util.Collection;

  13. import java.util.List;

  14. /**

  15. * 用户类

  16. * @author lihaodong

  17. * @since 2019-03-14

  18. */

  19. @Setter

  20. @Getter

  21. @ToString

  22. @TableName("user")

  23. public class User extends Model<User>{

  24. private static final long serialVersionUID = 1L;

  25. private Integer id;

  26. private String username;

  27. private String password;

  28. }

4.Role类

  1. package com.li.springbootsecurity.model;

  2. import com.baomidou.mybatisplus.annotation.TableName;

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

  4. import lombok.*;

  5. import lombok.experimental.Accessors;

  6. /**

  7. * 角色类

  8. * @author lihaodong

  9. * @since 2019-03-14

  10. */

  11. @Setter

  12. @Getter

  13. @Builder

  14. @TableName("role")

  15. public class Role extends Model<User> {

  16. private static final long serialVersionUID = 1L;

  17. private Integer id;

  18. private String name;

  19. }

4.用户服务类

  1. package com.li.springbootsecurity.service;

  2. import com.li.springbootsecurity.bo.ResponseUserToken;

  3. import com.li.springbootsecurity.model.User;

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

  5. import com.li.springbootsecurity.security.SecurityUser;

  6. /**

  7. * <p>

  8. * 用户服务类

  9. * </p>

  10. *

  11. * @author lihaodong

  12. * @since 2019-03-14

  13. */

  14. public interface IUserService extends IService<User> {

  15. /**

  16. * 通过用户名查找用户

  17. *

  18. * @param username 用户名

  19. * @return 用户信息

  20. */

  21. User findByUserName(String username);

  22. /**

  23. * 登陆

  24. * @param username

  25. * @param password

  26. * @return

  27. */

  28. ResponseUserToken login(String username, String password);

  29. /**

  30. * 根据Token获取用户信息

  31. * @param token

  32. * @return

  33. */

  34. SecurityUser getUserByToken(String token);

  35. }

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

  1. package com.li.springbootsecurity.security;

  2. import com.li.springbootsecurity.model.Role;

  3. import com.li.springbootsecurity.model.User;

  4. import lombok.Getter;

  5. import lombok.Setter;

  6. import org.springframework.security.core.GrantedAuthority;

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

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

  9. import java.util.ArrayList;

  10. import java.util.Collection;

  11. import java.util.Date;

  12. import java.util.List;

  13. /**

  14. * @Author 李号东

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

  16. * @Date 13:29 2019-03-16

  17. * @Param

  18. * @return

  19. **/

  20. @Setter

  21. @Getter

  22. public class SecurityUser extends User implements UserDetails {

  23. private static final long serialVersionUID = 1L;

  24. private Integer id;

  25. private String username;

  26. private String password;

  27. private Role role;

  28. private Date lastPasswordResetDate;

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

  30. this.id = id;

  31. this.username = username;

  32. this.password = password;

  33. this.role = role;

  34. }

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

  36. this.username = username;

  37. this.password = password;

  38. this.role = role;

  39. }

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

  41. this.id = id;

  42. this.username = username;

  43. this.password = password;

  44. }

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

  46. @Override

  47. public Collection<? extends GrantedAuthority> getAuthorities() {

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

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

  50. return authorities;

  51. }

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

  53. @Override

  54. public boolean isAccountNonExpired() {

  55. return true;

  56. }

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

  58. @Override

  59. public boolean isAccountNonLocked() {

  60. return true;

  61. }

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

  63. @Override

  64. public boolean isCredentialsNonExpired() {

  65. return true;

  66. }

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

  68. @Override

  69. public boolean isEnabled() {

  70. return true;

  71. }

  72. }

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

6.创建JWT工具类

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

  1. package com.li.springbootsecurity.utils;

  2. import com.alibaba.fastjson.JSON;

  3. import com.li.springbootsecurity.model.Role;

  4. import com.li.springbootsecurity.security.SecurityUser;

  5. import io.jsonwebtoken.CompressionCodecs;

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

  7. import org.springframework.security.core.GrantedAuthority;

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

  9. import org.springframework.stereotype.Component;

  10. import java.util.*;

  11. import java.util.concurrent.ConcurrentHashMap;

  12. import io.jsonwebtoken.Claims;

  13. import io.jsonwebtoken.Jwts;

  14. import io.jsonwebtoken.SignatureAlgorithm;

  15. /**

  16. * @Classname JwtTokenUtil

  17. * @Description JWT工具类

  18. * @Author 李号东 lihaodongmail@163.com

  19. * @Date 2019-03-14 14:54

  20. * @Version 1.0

  21. */

  22. @Component

  23. public class JwtTokenUtil {

  24. private static final String ROLE_REFRESH_TOKEN = "ROLE_REFRESH_TOKEN";

  25. private static final String CLAIM_KEY_USER_ID = "user_id";

  26. private static final String CLAIM_KEY_AUTHORITIES = "scope";

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

  28. /**

  29. * 密钥

  30. */

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

  32. private String secret;

  33. /**

  34. * 有效期

  35. */

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

  37. private Long accessTokenExpiration;

  38. /**

  39. * 刷新有效期

  40. */

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

  42. private Long refreshTokenExpiration;

  43. private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;

  44. /**

  45. * 根据token 获取用户信息

  46. * @param token

  47. * @return

  48. */

  49. public SecurityUser getUserFromToken(String token) {

  50. SecurityUser userDetail;

  51. try {

  52. final Claims claims = getClaimsFromToken(token);

  53. int userId = getUserIdFromToken(token);

  54. String username = claims.getSubject();

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

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

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

  58. } catch (Exception e) {

  59. userDetail = null;

  60. }

  61. return userDetail;

  62. }

  63. /**

  64. * 根据token 获取用户ID

  65. * @param token

  66. * @return

  67. */

  68. private int getUserIdFromToken(String token) {

  69. int userId;

  70. try {

  71. final Claims claims = getClaimsFromToken(token);

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

  73. } catch (Exception e) {

  74. userId = 0;

  75. }

  76. return userId;

  77. }

  78. /**

  79. * 根据token 获取用户名

  80. * @param token

  81. * @return

  82. */

  83. public String getUsernameFromToken(String token) {

  84. String username;

  85. try {

  86. final Claims claims = getClaimsFromToken(token);

  87. username = claims.getSubject();

  88. } catch (Exception e) {

  89. username = null;

  90. }

  91. return username;

  92. }

  93. /**

  94. * 根据token 获取生成时间

  95. * @param token

  96. * @return

  97. */

  98. public Date getCreatedDateFromToken(String token) {

  99. Date created;

  100. try {

  101. final Claims claims = getClaimsFromToken(token);

  102. created = claims.getIssuedAt();

  103. } catch (Exception e) {

  104. created = null;

  105. }

  106. return created;

  107. }

  108. /**

  109. * 生成令牌

  110. *

  111. * @param userDetail 用户

  112. * @return 令牌

  113. */

  114. public String generateAccessToken(SecurityUser userDetail) {

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

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

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

  118. }

  119. /**

  120. * 根据token 获取过期时间

  121. * @param token

  122. * @return

  123. */

  124. private Date getExpirationDateFromToken(String token) {

  125. Date expiration;

  126. try {

  127. final Claims claims = getClaimsFromToken(token);

  128. expiration = claims.getExpiration();

  129. } catch (Exception e) {

  130. expiration = null;

  131. }

  132. return expiration;

  133. }

  134. public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {

  135. final Date created = getCreatedDateFromToken(token);

  136. return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)

  137. && (!isTokenExpired(token));

  138. }

  139. /**

  140. * 刷新令牌

  141. *

  142. * @param token 原令牌

  143. * @return 新令牌

  144. */

  145. public String refreshToken(String token) {

  146. String refreshedToken;

  147. try {

  148. final Claims claims = getClaimsFromToken(token);

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

  150. } catch (Exception e) {

  151. refreshedToken = null;

  152. }

  153. return refreshedToken;

  154. }

  155. /**

  156. * 验证token 是否合法

  157. * @param token token

  158. * @param userDetails 用户信息

  159. * @return

  160. */

  161. public boolean validateToken(String token, UserDetails userDetails) {

  162. SecurityUser userDetail = (SecurityUser) userDetails;

  163. final long userId = getUserIdFromToken(token);

  164. final String username = getUsernameFromToken(token);

  165. return (userId == userDetail.getId()

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

  167. && !isTokenExpired(token)

  168. );

  169. }

  170. /**

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

  172. * @param userDetail

  173. * @return

  174. */

  175. public String generateRefreshToken(SecurityUser userDetail) {

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

  177. // 只授于更新 token 的权限

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

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

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

  181. }

  182. public void putToken(String userName, String token) {

  183. tokenMap.put(userName, token);

  184. }

  185. public void deleteToken(String userName) {

  186. tokenMap.remove(userName);

  187. }

  188. public boolean containToken(String userName, String token) {

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

  190. }

  191. /***

  192. * 解析token 信息

  193. * @param token

  194. * @return

  195. */

  196. private Claims getClaimsFromToken(String token) {

  197. Claims claims;

  198. try {

  199. claims = Jwts.parser()

  200. .setSigningKey(secret)

  201. .parseClaimsJws(token)

  202. .getBody();

  203. } catch (Exception e) {

  204. claims = null;

  205. }

  206. return claims;

  207. }

  208. /**

  209. * 生成失效时间

  210. * @param expiration

  211. * @return

  212. */

  213. private Date generateExpirationDate(long expiration) {

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

  215. }

  216. /**

  217. * 判断令牌是否过期

  218. *

  219. * @param token 令牌

  220. * @return 是否过期

  221. */

  222. private Boolean isTokenExpired(String token) {

  223. final Date expiration = getExpirationDateFromToken(token);

  224. return expiration.before(new Date());

  225. }

  226. /**

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

  228. * @param created 生成时间

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

  230. * @return

  231. */

  232. private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {

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

  234. }

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

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

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

  238. return claims;

  239. }

  240. /**

  241. * 生成token

  242. * @param subject 用户名

  243. * @param claims

  244. * @return

  245. */

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

  247. return generateToken(subject, claims, accessTokenExpiration);

  248. }

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

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

  251. for (GrantedAuthority ga : authorities) {

  252. list.add(ga.getAuthority());

  253. }

  254. return list;

  255. }

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

  257. return generateToken(subject, claims, refreshTokenExpiration);

  258. }

  259. /**

  260. * 生成token

  261. * @param subject 用户名

  262. * @param claims

  263. * @param expiration 过期时间

  264. * @return

  265. */

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

  267. return Jwts.builder()

  268. .setClaims(claims)

  269. .setSubject(subject)

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

  271. .setIssuedAt(new Date())

  272. .setExpiration(generateExpirationDate(expiration))

  273. .compressWith(CompressionCodecs.DEFLATE)

  274. .signWith(SIGNATURE_ALGORITHM, secret)

  275. .compact();

  276. }

  277. }

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

  1. package com.li.springbootsecurity.config;

  2. import com.li.springbootsecurity.security.SecurityUser;

  3. import com.li.springbootsecurity.utils.JwtTokenUtil;

  4. import lombok.extern.slf4j.Slf4j;

  5. import org.apache.commons.lang3.StringUtils;

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

  7. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

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

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

  10. import org.springframework.stereotype.Component;

  11. import org.springframework.web.filter.OncePerRequestFilter;

  12. import javax.annotation.Resource;

  13. import javax.servlet.FilterChain;

  14. import javax.servlet.ServletException;

  15. import javax.servlet.http.HttpServletRequest;

  16. import javax.servlet.http.HttpServletResponse;

  17. import java.io.IOException;

  18. import java.util.Date;

  19. /**

  20. * @Author 李号东

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

  22. * @Date 00:32 2019-03-17

  23. * @Param

  24. * @return

  25. **/

  26. @Slf4j

  27. @Component

  28. public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

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

  30. private String tokenHeader;

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

  32. private String authTokenStart;

  33. @Resource

  34. private JwtTokenUtil jwtTokenUtil;

  35. @Override

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

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

  38. System.out.println(authToken);

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

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

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

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

  43. //获取token生成时间

  44. Date createTokenDate = jwtTokenUtil.getCreatedDateFromToken(authToken);

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

  46. } else {

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

  48. authToken = null;

  49. }

  50. String username = jwtTokenUtil.getUsernameFromToken(authToken);

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

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

  53. SecurityUser userDetail = jwtTokenUtil.getUserFromToken(authToken);

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

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

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

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

  58. SecurityContextHolder.getContext().setAuthentication(authentication);

  59. }

  60. }

  61. chain.doFilter(request, response);

  62. }

  63. }

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

  1. package com.li.springbootsecurity.config;

  2. import com.li.springbootsecurity.bo.ResultCode;

  3. import com.li.springbootsecurity.bo.ResultJson;

  4. import com.li.springbootsecurity.bo.ResultUtil;

  5. import lombok.extern.slf4j.Slf4j;

  6. import org.springframework.security.access.AccessDeniedException;

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

  8. import org.springframework.stereotype.Component;

  9. import javax.servlet.http.HttpServletRequest;

  10. import javax.servlet.http.HttpServletResponse;

  11. import java.io.IOException;

  12. import java.io.PrintWriter;

  13. /**

  14. * @Author 李号东

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

  16. * @Date 00:31 2019-03-17

  17. * @Param

  18. * @return

  19. **/

  20. @Slf4j

  21. @Component("RestAuthenticationAccessDeniedHandler")

  22. public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler {

  23. @Override

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

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

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

  27. log.info(msg.toString());

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

  29. }

  30. }

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

  1. package com.li.springbootsecurity.config;

  2. import com.li.springbootsecurity.bo.ResultCode;

  3. import com.li.springbootsecurity.bo.ResultUtil;

  4. import lombok.extern.slf4j.Slf4j;

  5. import org.springframework.security.authentication.BadCredentialsException;

  6. import org.springframework.security.authentication.InsufficientAuthenticationException;

  7. import org.springframework.security.core.AuthenticationException;

  8. import org.springframework.security.web.AuthenticationEntryPoint;

  9. import org.springframework.stereotype.Component;

  10. import javax.servlet.http.HttpServletRequest;

  11. import javax.servlet.http.HttpServletResponse;

  12. import java.io.IOException;

  13. import java.io.Serializable;

  14. /**

  15. * @Author 李号东

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

  17. * @Date 00:32 2019-03-17

  18. * @Param

  19. * @return

  20. **/

  21. @Slf4j

  22. @Component

  23. public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

  24. private static final long serialVersionUID = -8970718410437077606L;

  25. @Override

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

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

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

  29. log.info(msg.toString());

  30. log.info(e.toString());

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

  32. if (e instanceof BadCredentialsException) {

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

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

  35. } else if (e instanceof InsufficientAuthenticationException) {

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

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

  38. } else {

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

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

  41. }

  42. }

  43. }

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

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

  1. package com.li.springbootsecurity.config;

  2. import com.li.springbootsecurity.model.Role;

  3. import com.li.springbootsecurity.model.User;

  4. import com.li.springbootsecurity.security.SecurityUser;

  5. import com.li.springbootsecurity.service.IRoleService;

  6. import com.li.springbootsecurity.service.IUserService;

  7. import lombok.extern.slf4j.Slf4j;

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

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

  10. import org.springframework.context.annotation.Bean;

  11. import org.springframework.context.annotation.Configuration;

  12. import org.springframework.security.authentication.AuthenticationManager;

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

  14. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

  15. import org.springframework.security.config.annotation.web.builders.HttpSecurity;

  16. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

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

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

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

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

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

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

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

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

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

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

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

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

  29. /**

  30. * @Author 李号东

  31. * @Description Security配置类

  32. * @Date 00:36 2019-03-17

  33. * @Param

  34. * @return

  35. **/

  36. @Slf4j

  37. @Configuration

  38. @EnableWebSecurity //启动web安全性

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

  40. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  41. @Autowired

  42. private JwtAuthenticationEntryPoint unauthorizedHandler;

  43. @Autowired

  44. private AccessDeniedHandler accessDeniedHandler;

  45. @Autowired

  46. private JwtAuthenticationTokenFilter authenticationTokenFilter;

  47. /**

  48. * 解决 无法直接注入 AuthenticationManager

  49. * @return

  50. * @throws Exception

  51. */

  52. @Bean

  53. @Override

  54. public AuthenticationManager authenticationManagerBean() throws Exception {

  55. return super.authenticationManagerBean();

  56. }

  57. @Autowired

  58. public WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler,

  59. @Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler,

  60. JwtAuthenticationTokenFilter authenticationTokenFilter) {

  61. this.unauthorizedHandler = unauthorizedHandler;

  62. this.accessDeniedHandler = accessDeniedHandler;

  63. this.authenticationTokenFilter = authenticationTokenFilter;

  64. }

  65. /**

  66. * 配置策略

  67. *

  68. * @param httpSecurity

  69. * @throws Exception

  70. */

  71. @Override

  72. protected void configure(HttpSecurity httpSecurity) throws Exception {

  73. httpSecurity

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

  75. .csrf().disable()

  76. // 权限不足处理类

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

  78. // 认证失败处理类

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

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

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

  82. .authorizeRequests()

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

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

  85. // 需要拥有admin权限

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

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

  88. .anyRequest().authenticated();

  89. // 禁用缓存

  90. httpSecurity.headers().cacheControl();

  91. // 添加JWT filter

  92. httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

  93. }

  94. @Autowired

  95. public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

  96. auth

  97. // 设置UserDetailsService

  98. .userDetailsService(userDetailsService())

  99. // 使用BCrypt进行密码的hash

  100. .passwordEncoder(passwordEncoder());

  101. auth.eraseCredentials(false);

  102. }

  103. /**

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

  105. *

  106. * @return

  107. */

  108. @Bean

  109. public BCryptPasswordEncoder passwordEncoder() {

  110. return new BCryptPasswordEncoder();

  111. }

  112. /**

  113. * 登陆身份认证

  114. *

  115. * @return

  116. */

  117. @Override

  118. @Bean

  119. public UserDetailsService userDetailsService() {

  120. return new UserDetailsService() {

  121. @Autowired

  122. private IUserService userService;

  123. @Autowired

  124. private IRoleService roleService;

  125. @Override

  126. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

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

  128. User user = userService.findByUserName(username);

  129. if (user == null) {

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

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

  132. }

  133. //获取用户拥有的角色

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

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

  136. }

  137. };

  138. }

  139. }

11.创建测试的 LoginController:

  1. package com.li.springbootsecurity.controller;

  2. import com.li.springbootsecurity.bo.ResponseUseroken;

  3. import com.li.springbootsecurity.bo.ResultCode;

  4. import com.li.springbootsecurity.bo.ResultJson;

  5. import com.li.springbootsecurity.model.User;

  6. import com.li.springbootsecurity.security.SecurityUser;

  7. import com.li.springbootsecurity.service.IUserService;

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

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

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

  11. import org.springframework.stereotype.Controller;

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

  13. import javax.servlet.http.HttpServletRequest;

  14. /**

  15. * @Classname LoginController

  16. * @Description 测试

  17. * @Author 李号东 lihaodongmail@163.com

  18. * @Date 2019-03-16 10:06

  19. * @Version 1.0

  20. */

  21. @Controller

  22. public class LoginController {

  23. @Autowired

  24. private IUserService userService;

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

  26. private String tokenHeader;

  27. /**

  28. * @Author 李号东

  29. * @Description 登录

  30. * @Date 10:18 2019-03-17

  31. * @Param [user]

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

  33. **/

  34. @RequestMapping(value = "/login")

  35. @ResponseBody

  36. public ResultJson<ResponseUserToken> login(User user) {

  37. System.out.println(user);

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

  39. return ResultJson.ok(response);

  40. }

  41. /**

  42. * @Author 李号东

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

  44. * @Date 10:17 2019-03-17

  45. * @Param [request]

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

  47. **/

  48. @GetMapping(value = "/user")

  49. @ResponseBody

  50. public ResultJson getUser(HttpServletRequest request) {

  51. String token = request.getHeader(tokenHeader);

  52. if (token == null) {

  53. return ResultJson.failure(ResultCode.UNAUTHORIZED);

  54. }

  55. SecurityUser securityUser = userService.getUserByToken(token);

  56. return ResultJson.ok(securityUser);

  57. }

  58. public static void main(String[] args) {

  59. String password = "admin";

  60. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(4);

  61. String enPassword = encoder.encode(password);

  62. System.out.println(enPassword);

  63. }

  64. }

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

权限测试 访问/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权限鉴权系统的更多相关文章

  1. Spring Boot(十四):spring boot整合shiro-登录认证和权限管理

    Spring Boot(十四):spring boot整合shiro-登录认证和权限管理 使用Spring Boot集成Apache Shiro.安全应该是互联网公司的一道生命线,几乎任何的公司都会涉 ...

  2. (转)Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理

    http://www.ityouknow.com/springboot/2017/06/26/spring-boot-shiro.html 这篇文章我们来学习如何使用 Spring Boot 集成 A ...

  3. Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理

    这篇文章我们来学习如何使用 Spring Boot 集成 Apache Shiro .安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求.在 Java 领域一般有 Spring S ...

  4. Spring Boot整合shiro-登录认证和权限管理

    原文地址:http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html 这篇文章我们来学习如何使用Spring Boot集成 ...

  5. Spring Boot 整合 Shiro-登录认证和权限管理

    这篇文章我们来学习如何使用 Spring Boot 集成 Apache Shiro .安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求.在 Java 领域一般有 Spring S ...

  6. Spring Boot (十三): Spring Boot 整合 RabbitMQ

    1. 前言 RabbitMQ 是一个消息队列,说到消息队列,大家可能多多少少有听过,它主要的功能是用来实现应用服务的异步与解耦,同时也能起到削峰填谷.消息分发的作用. 消息队列在比较主要的一个作用是用 ...

  7. Spring Boot整合Thymeleaf视图层

    目录 Spring Boot整合Thymeleaf Spring Boot整合Thymeleaf 的项目步骤 Thymeleaf 语法详解 Spring Boot整合Thymeleaf Spring ...

  8. Spring Boot 整合视图层技术,application全局配置文件

    目录 Spring Boot 整合视图层技术 Spring Boot 整合jsp Spring Boot 整合freemarker Spring Boot 整合视图层技术 Spring Boot 整合 ...

  9. Spring Boot中使用 Spring Security 构建权限系统

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全 ...

随机推荐

  1. 2016北京集训 小Q与进位制

    题目大意 一个数每一位进制不同,已知每一位的进制,求该数的十进制表达. 显然有 $$Ans=\sum\limits_{i=0}^{n-1}a_i \prod\limits_{j=0}^{i-1}bas ...

  2. 1147. Heaps (30)

    In computer science, a heap is a specialized tree-based data structure that satisfies the heap prope ...

  3. ACM学习历程——ZOJ 3829 Known Notation (2014牡丹江区域赛K题)(策略,栈)

    Description Do you know reverse Polish notation (RPN)? It is a known notation in the area of mathema ...

  4. poj2420 A Star not a Tree? 模拟退火

    题目大意: 给定n个点,求一个点,使其到这n个点的距离最小.(\(n \leq 100\)) 题解 模拟退火上 #include <cmath> #include <cstdio&g ...

  5. 闪回之 回收站、Flashback Drop (table、index、trigger等)

    一: Flashback Drop 操作流程 模式一:drop table 后未新建同名表 SQL> create table flashdrop as select * from user_o ...

  6. bzoj 2406 矩阵 —— 有源汇上下界可行流

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=2406 这题,首先把题目那个式子的绝对值拆成两个限制,就成了网络流的上下界: 有上下界可行流原 ...

  7. hdoj1113(字符串map应用)

    #include<iostream> #include<cstdio> #include<string> #include<cstring> #incl ...

  8. java用write()拷贝一个文本文件

    总结:灵活运用循环语句,或条件判断语句.每一种流的正确使用方法: 这里是两种方法: package com.ds; import java.io.*; public class tyut { /*pu ...

  9. Code:目录

    ylbtech-Code:目录 1.返回顶部 1. https://github.com/   2.返回顶部 1. https://gitee.com 2. 3.返回顶部   4.返回顶部   5.返 ...

  10. Python模块-chardet模块

    chardet模块用来获取文件的编码 # -*- coding:utf-8 -*- __author__ = "MuT6 Sch01aR" import chardet f = o ...