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

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

  12. <dependency>

  13. <groupId>mysql</groupId>

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

  15. <scope>runtime</scope>

  16. </dependency>

  17. <!--Mybatis-Plus-->

  18. <dependency>

  19. <groupId>com.baomidou</groupId>

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

  21. <version>3.0.6</version>

  22. </dependency>

  23. <dependency>

  24. <groupId>com.baomidou</groupId>

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

  26. <version>3.0.6</version>

  27. </dependency>

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

  29. <dependency>

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

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

  32. <version>2.0</version>

  33. </dependency>

  34. <!--JWT-->

  35. <dependency>

  36. <groupId>io.jsonwebtoken</groupId>

  37. <artifactId>jjwt</artifactId>

  38. <version>0.9.0</version>

  39. </dependency>

  40. <!--lombok-->

  41. <dependency>

  42. <groupId>org.projectlombok</groupId>

  43. <artifactId>lombok</artifactId>

  44. <optional>true</optional>

  45. </dependency>

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

  47. <dependency>

  48. <groupId>com.alibaba</groupId>

  49. <artifactId>fastjson</artifactId>

  50. <version>1.2.4</version>

  51. </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.  

  7. #mybatis-plus配置

  8. #mapper对应文件

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

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

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

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

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

  14.  

  15. #Hikari 连接池配置

  16. #最小空闲连接数量

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

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

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

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

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

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

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

  24. #连接池名字

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

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

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

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

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

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

  31.  

  32. # JWT配置

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

  34. jwt.secret=mySecret

  35. # 头部

  36. jwt.header=Authorization

  37. # token有效时间

  38. jwt.expiration=604800

  39. # token头部

  40. jwt.tokenHead=Bearer

2.代码生成

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

生成代码

  1. package com.li.springbootsecurity.code;

  2.  

  3.  

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

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

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

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

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

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

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

  11.  

  12.  

  13. /**

  14. * @Author 李号东

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

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

  17. * @Param

  18. * @return

  19. **/

  20. public class MyBatisPlusGenerator {

  21.  

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

  23. // 代码生成器

  24. AutoGenerator mpg = new AutoGenerator();

  25.  

  26. //1. 全局配置

  27. GlobalConfig gc = new GlobalConfig();

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

  29. gc.setOpen(false);

  30. gc.setFileOverride(true);

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

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

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

  34. mpg.setGlobalConfig(gc);

  35.  

  36. //2. 数据源配置

  37. DataSourceConfig dsc = new DataSourceConfig();

  38. dsc.setDbType(DbType.MYSQL);

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

  40. dsc.setUsername("root");

  41. dsc.setPassword("root");

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

  43. mpg.setDataSource(dsc);

  44.  

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

  46. StrategyConfig strategy = new StrategyConfig();

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

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

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

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

  51. strategy.setEntityLombokModel(true);

  52. strategy.setRestControllerStyle(true);

  53. strategy.setControllerMappingHyphenStyle(true);

  54.  

  55. mpg.setStrategy(strategy);

  56.  

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

  58. PackageConfig pc = new PackageConfig();

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

  60. pc.setEntity("model");

  61. mpg.setPackageInfo(pc);

  62.  

  63. // 执行生成

  64. mpg.execute();

  65.  

  66. }

  67. }

3.User类

简单的用户模型

  1. package com.li.springbootsecurity.model;

  2.  

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

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

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

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

  7. import lombok.*;

  8. import lombok.experimental.Accessors;

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

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

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

  12.  

  13. import java.util.ArrayList;

  14. import java.util.Collection;

  15. import java.util.List;

  16.  

  17. /**

  18. * 用户类

  19. * @author lihaodong

  20. * @since 2019-03-14

  21. */

  22. @Setter

  23. @Getter

  24. @ToString

  25. @TableName("user")

  26. public class User extends Model<User>{

  27.  

  28. private static final long serialVersionUID = 1L;

  29.  

  30. private Integer id;

  31.  

  32. private String username;

  33.  

  34. private String password;

  35.  

  36. }

4.Role类

  1. package com.li.springbootsecurity.model;

  2.  

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

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

  5. import lombok.*;

  6. import lombok.experimental.Accessors;

  7.  

  8. /**

  9. * 角色类

  10. * @author lihaodong

  11. * @since 2019-03-14

  12. */

  13. @Setter

  14. @Getter

  15. @Builder

  16. @TableName("role")

  17. public class Role extends Model<User> {

  18.  

  19. private static final long serialVersionUID = 1L;

  20.  

  21. private Integer id;

  22.  

  23. private String name;

  24.  

  25.  

  26. }

4.用户服务类

  1. package com.li.springbootsecurity.service;

  2.  

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

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

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

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

  7.  

  8. /**

  9. * <p>

  10. * 用户服务类

  11. * </p>

  12. *

  13. * @author lihaodong

  14. * @since 2019-03-14

  15. */

  16. public interface IUserService extends IService<User> {

  17.  

  18.  

  19. /**

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

  21. *

  22. * @param username 用户名

  23. * @return 用户信息

  24. */

  25. User findByUserName(String username);

  26.  

  27. /**

  28. * 登陆

  29. * @param username

  30. * @param password

  31. * @return

  32. */

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

  34.  

  35.  

  36. /**

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

  38. * @param token

  39. * @return

  40. */

  41. SecurityUser getUserByToken(String token);

  42. }

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

  1. package com.li.springbootsecurity.security;

  2.  

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

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

  5. import lombok.Getter;

  6. import lombok.Setter;

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

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

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

  10.  

  11. import java.util.ArrayList;

  12. import java.util.Collection;

  13. import java.util.Date;

  14. import java.util.List;

  15.  

  16. /**

  17. * @Author 李号东

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

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

  20. * @Param

  21. * @return

  22. **/

  23. @Setter

  24. @Getter

  25. public class SecurityUser extends User implements UserDetails {

  26. private static final long serialVersionUID = 1L;

  27.  

  28. private Integer id;

  29. private String username;

  30. private String password;

  31. private Role role;

  32. private Date lastPasswordResetDate;

  33.  

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

  35. this.id = id;

  36. this.username = username;

  37. this.password = password;

  38. this.role = role;

  39. }

  40.  

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

  42. this.username = username;

  43. this.password = password;

  44. this.role = role;

  45. }

  46.  

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

  48. this.id = id;

  49. this.username = username;

  50. this.password = password;

  51. }

  52.  

  53.  

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

  55. @Override

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

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

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

  59. return authorities;

  60. }

  61.  

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

  63. @Override

  64. public boolean isAccountNonExpired() {

  65. return true;

  66. }

  67.  

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

  69. @Override

  70. public boolean isAccountNonLocked() {

  71. return true;

  72. }

  73.  

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

  75. @Override

  76. public boolean isCredentialsNonExpired() {

  77. return true;

  78. }

  79.  

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

  81. @Override

  82. public boolean isEnabled() {

  83. return true;

  84. }

  85. }

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

6.创建JWT工具类

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

  1. package com.li.springbootsecurity.utils;

  2.  

  3. import com.alibaba.fastjson.JSON;

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

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

  6. import io.jsonwebtoken.CompressionCodecs;

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

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

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

  10. import org.springframework.stereotype.Component;

  11.  

  12. import java.util.*;

  13. import java.util.concurrent.ConcurrentHashMap;

  14.  

  15. import io.jsonwebtoken.Claims;

  16. import io.jsonwebtoken.Jwts;

  17. import io.jsonwebtoken.SignatureAlgorithm;

  18.  

  19. /**

  20. * @Classname JwtTokenUtil

  21. * @Description JWT工具类

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

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

  24. * @Version 1.0

  25. */

  26. @Component

  27. public class JwtTokenUtil {

  28.  

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

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

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

  32.  

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

  34.  

  35. /**

  36. * 密钥

  37. */

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

  39. private String secret;

  40.  

  41. /**

  42. * 有效期

  43. */

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

  45. private Long accessTokenExpiration;

  46.  

  47. /**

  48. * 刷新有效期

  49. */

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

  51. private Long refreshTokenExpiration;

  52.  

  53. private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;

  54.  

  55.  

  56. /**

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

  58. * @param token

  59. * @return

  60. */

  61. public SecurityUser getUserFromToken(String token) {

  62. SecurityUser userDetail;

  63. try {

  64. final Claims claims = getClaimsFromToken(token);

  65. int userId = getUserIdFromToken(token);

  66. String username = claims.getSubject();

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

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

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

  70. } catch (Exception e) {

  71. userDetail = null;

  72. }

  73. return userDetail;

  74. }

  75.  

  76. /**

  77. * 根据token 获取用户ID

  78. * @param token

  79. * @return

  80. */

  81. private int getUserIdFromToken(String token) {

  82. int userId;

  83. try {

  84. final Claims claims = getClaimsFromToken(token);

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

  86. } catch (Exception e) {

  87. userId = 0;

  88. }

  89. return userId;

  90. }

  91.  

  92.  

  93. /**

  94. * 根据token 获取用户名

  95. * @param token

  96. * @return

  97. */

  98. public String getUsernameFromToken(String token) {

  99. String username;

  100. try {

  101. final Claims claims = getClaimsFromToken(token);

  102. username = claims.getSubject();

  103. } catch (Exception e) {

  104. username = null;

  105. }

  106. return username;

  107. }

  108.  

  109. /**

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

  111. * @param token

  112. * @return

  113. */

  114. public Date getCreatedDateFromToken(String token) {

  115. Date created;

  116. try {

  117. final Claims claims = getClaimsFromToken(token);

  118. created = claims.getIssuedAt();

  119. } catch (Exception e) {

  120. created = null;

  121. }

  122. return created;

  123. }

  124.  

  125.  

  126. /**

  127. * 生成令牌

  128. *

  129. * @param userDetail 用户

  130. * @return 令牌

  131. */

  132. public String generateAccessToken(SecurityUser userDetail) {

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

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

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

  136. }

  137.  

  138. /**

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

  140. * @param token

  141. * @return

  142. */

  143. private Date getExpirationDateFromToken(String token) {

  144. Date expiration;

  145. try {

  146. final Claims claims = getClaimsFromToken(token);

  147. expiration = claims.getExpiration();

  148. } catch (Exception e) {

  149. expiration = null;

  150. }

  151. return expiration;

  152. }

  153.  

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

  155. final Date created = getCreatedDateFromToken(token);

  156. return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)

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

  158. }

  159.  

  160. /**

  161. * 刷新令牌

  162. *

  163. * @param token 原令牌

  164. * @return 新令牌

  165. */

  166. public String refreshToken(String token) {

  167. String refreshedToken;

  168. try {

  169. final Claims claims = getClaimsFromToken(token);

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

  171. } catch (Exception e) {

  172. refreshedToken = null;

  173. }

  174. return refreshedToken;

  175. }

  176.  

  177.  

  178. /**

  179. * 验证token 是否合法

  180. * @param token token

  181. * @param userDetails 用户信息

  182. * @return

  183. */

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

  185. SecurityUser userDetail = (SecurityUser) userDetails;

  186. final long userId = getUserIdFromToken(token);

  187. final String username = getUsernameFromToken(token);

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

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

  190. && !isTokenExpired(token)

  191. );

  192. }

  193.  

  194. /**

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

  196. * @param userDetail

  197. * @return

  198. */

  199. public String generateRefreshToken(SecurityUser userDetail) {

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

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

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

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

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

  205. }

  206.  

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

  208. tokenMap.put(userName, token);

  209. }

  210.  

  211. public void deleteToken(String userName) {

  212. tokenMap.remove(userName);

  213. }

  214.  

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

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

  217. }

  218.  

  219. /***

  220. * 解析token 信息

  221. * @param token

  222. * @return

  223. */

  224. private Claims getClaimsFromToken(String token) {

  225. Claims claims;

  226. try {

  227. claims = Jwts.parser()

  228. .setSigningKey(secret)

  229. .parseClaimsJws(token)

  230. .getBody();

  231. } catch (Exception e) {

  232. claims = null;

  233. }

  234. return claims;

  235. }

  236.  

  237. /**

  238. * 生成失效时间

  239. * @param expiration

  240. * @return

  241. */

  242. private Date generateExpirationDate(long expiration) {

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

  244. }

  245.  

  246. /**

  247. * 判断令牌是否过期

  248. *

  249. * @param token 令牌

  250. * @return 是否过期

  251. */

  252. private Boolean isTokenExpired(String token) {

  253. final Date expiration = getExpirationDateFromToken(token);

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

  255. }

  256.  

  257. /**

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

  259. * @param created 生成时间

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

  261. * @return

  262. */

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

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

  265. }

  266.  

  267.  

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

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

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

  271. return claims;

  272. }

  273.  

  274. /**

  275. * 生成token

  276. * @param subject 用户名

  277. * @param claims

  278. * @return

  279. */

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

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

  282. }

  283.  

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

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

  286. for (GrantedAuthority ga : authorities) {

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

  288. }

  289. return list;

  290. }

  291.  

  292.  

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

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

  295. }

  296.  

  297.  

  298. /**

  299. * 生成token

  300. * @param subject 用户名

  301. * @param claims

  302. * @param expiration 过期时间

  303. * @return

  304. */

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

  306. return Jwts.builder()

  307. .setClaims(claims)

  308. .setSubject(subject)

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

  310. .setIssuedAt(new Date())

  311. .setExpiration(generateExpirationDate(expiration))

  312. .compressWith(CompressionCodecs.DEFLATE)

  313. .signWith(SIGNATURE_ALGORITHM, secret)

  314. .compact();

  315. }

  316.  

  317. }

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

  1. package com.li.springbootsecurity.config;

  2.  

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

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

  5. import lombok.extern.slf4j.Slf4j;

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

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

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

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

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

  11. import org.springframework.stereotype.Component;

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

  13.  

  14. import javax.annotation.Resource;

  15. import javax.servlet.FilterChain;

  16. import javax.servlet.ServletException;

  17. import javax.servlet.http.HttpServletRequest;

  18. import javax.servlet.http.HttpServletResponse;

  19. import java.io.IOException;

  20. import java.util.Date;

  21.  

  22. /**

  23. * @Author 李号东

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

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

  26. * @Param

  27. * @return

  28. **/

  29. @Slf4j

  30. @Component

  31. public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

  32.  

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

  34. private String tokenHeader;

  35.  

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

  37. private String authTokenStart;

  38.  

  39. @Resource

  40. private JwtTokenUtil jwtTokenUtil;

  41.  

  42.  

  43. @Override

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

  45.  

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

  47. System.out.println(authToken);

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

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

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

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

  52. //获取token生成时间

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

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

  55.  

  56. } else {

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

  58. authToken = null;

  59. }

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

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

  62.  

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

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

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

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

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

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

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

  70. }

  71. }

  72. chain.doFilter(request, response);

  73. }

  74. }

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

  1. package com.li.springbootsecurity.config;

  2.  

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

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

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

  6. import lombok.extern.slf4j.Slf4j;

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

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

  9. import org.springframework.stereotype.Component;

  10.  

  11. import javax.servlet.http.HttpServletRequest;

  12. import javax.servlet.http.HttpServletResponse;

  13. import java.io.IOException;

  14. import java.io.PrintWriter;

  15.  

  16. /**

  17. * @Author 李号东

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

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

  20. * @Param

  21. * @return

  22. **/

  23. @Slf4j

  24. @Component("RestAuthenticationAccessDeniedHandler")

  25. public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler {

  26.  

  27. @Override

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

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

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

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

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

  33. }

  34. }

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

  1. package com.li.springbootsecurity.config;

  2.  

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

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

  5. import lombok.extern.slf4j.Slf4j;

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

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

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

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

  10. import org.springframework.stereotype.Component;

  11.  

  12. import javax.servlet.http.HttpServletRequest;

  13. import javax.servlet.http.HttpServletResponse;

  14. import java.io.IOException;

  15. import java.io.Serializable;

  16.  

  17. /**

  18. * @Author 李号东

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

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

  21. * @Param

  22. * @return

  23. **/

  24. @Slf4j

  25. @Component

  26. public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

  27.  

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

  29.  

  30. @Override

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

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

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

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

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

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

  37. if (e instanceof BadCredentialsException) {

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

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

  40. } else if (e instanceof InsufficientAuthenticationException) {

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

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

  43. } else {

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

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

  46. }

  47.  

  48. }

  49. }

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

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

  1. package com.li.springbootsecurity.config;

  2.  

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

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

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

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

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

  8. import lombok.extern.slf4j.Slf4j;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  30.  

  31.  

  32. /**

  33. * @Author 李号东

  34. * @Description Security配置类

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

  36. * @Param

  37. * @return

  38. **/

  39. @Slf4j

  40. @Configuration

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

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

  43. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  44.  

  45. @Autowired

  46. private JwtAuthenticationEntryPoint unauthorizedHandler;

  47.  

  48. @Autowired

  49. private AccessDeniedHandler accessDeniedHandler;

  50.  

  51. @Autowired

  52. private JwtAuthenticationTokenFilter authenticationTokenFilter;

  53.  

  54.  

  55. /**

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

  57. * @return

  58. * @throws Exception

  59. */

  60. @Bean

  61. @Override

  62. public AuthenticationManager authenticationManagerBean() throws Exception {

  63. return super.authenticationManagerBean();

  64. }

  65.  

  66. @Autowired

  67. public WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler,

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

  69. JwtAuthenticationTokenFilter authenticationTokenFilter) {

  70. this.unauthorizedHandler = unauthorizedHandler;

  71. this.accessDeniedHandler = accessDeniedHandler;

  72. this.authenticationTokenFilter = authenticationTokenFilter;

  73. }

  74.  

  75.  

  76. /**

  77. * 配置策略

  78. *

  79. * @param httpSecurity

  80. * @throws Exception

  81. */

  82. @Override

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

  84. httpSecurity

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

  86. .csrf().disable()

  87. // 权限不足处理类

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

  89. // 认证失败处理类

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

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

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

  93. .authorizeRequests()

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

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

  96. // 需要拥有admin权限

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

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

  99. .anyRequest().authenticated();

  100.  

  101. // 禁用缓存

  102. httpSecurity.headers().cacheControl();

  103. // 添加JWT filter

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

  105. }

  106.  

  107. @Autowired

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

  109. auth

  110. // 设置UserDetailsService

  111. .userDetailsService(userDetailsService())

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

  113. .passwordEncoder(passwordEncoder());

  114. auth.eraseCredentials(false);

  115. }

  116.  

  117. /**

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

  119. *

  120. * @return

  121. */

  122. @Bean

  123. public BCryptPasswordEncoder passwordEncoder() {

  124. return new BCryptPasswordEncoder();

  125. }

  126.  

  127. /**

  128. * 登陆身份认证

  129. *

  130. * @return

  131. */

  132. @Override

  133. @Bean

  134. public UserDetailsService userDetailsService() {

  135. return new UserDetailsService() {

  136. @Autowired

  137. private IUserService userService;

  138. @Autowired

  139. private IRoleService roleService;

  140.  

  141. @Override

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

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

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

  145. if (user == null) {

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

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

  148. }

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

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

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

  152. }

  153. };

  154. }

  155.  

  156.  

  157. }

11.创建测试的 LoginController:

  1. package com.li.springbootsecurity.controller;

  2.  

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

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

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

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

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

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

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

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

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

  12. import org.springframework.stereotype.Controller;

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

  14.  

  15. import javax.servlet.http.HttpServletRequest;

  16.  

  17. /**

  18. * @Classname LoginController

  19. * @Description 测试

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

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

  22. * @Version 1.0

  23. */

  24. @Controller

  25. public class LoginController {

  26.  

  27. @Autowired

  28. private IUserService userService;

  29.  

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

  31. private String tokenHeader;

  32.  

  33. /**

  34. * @Author 李号东

  35. * @Description 登录

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

  37. * @Param [user]

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

  39. **/

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

  41. @ResponseBody

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

  43. System.out.println(user);

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

  45. return ResultJson.ok(response);

  46. }

  47.  

  48. /**

  49. * @Author 李号东

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

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

  52. * @Param [request]

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

  54. **/

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

  56. @ResponseBody

  57. public ResultJson getUser(HttpServletRequest request) {

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

  59. if (token == null) {

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

  61. }

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

  63. return ResultJson.ok(securityUser);

  64. }

  65.  

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

  67. String password = "admin";

  68. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(4);

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

  70. System.out.println(enPassword);

  71. }

  72. }

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

权限测试 访问/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