写在前面
  
  关于 Spring Security Web系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求。在Java EE领域,成熟的安全框架解决方案一般有 Apache Shiro、Spring Security等两种技术选型。Apache Shiro简单易用也算是一大优势,但其功能还是远不如 Spring Security强大。Spring Security可以为 Spring 应用提供声明式的安全访问控制,起通过提供一系列可以在 Spring应用上下文中可配置的Bean,并利用 Spring IoC和 AOP等功能特性来为应用系统提供声明式的安全访问控制功能,减少了诸多重复工作。
  
  关于JWT JSON Web Token (JWT),是在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于作为JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息。关于JWT的科普,可以看看阮一峰老师的《JSON Web Token 入门教程》。
  
  本文则结合 Spring Security和 JWT两大利器来打造一个简易的权限系统。
  
  本文实验环境如下:
  
  Spring Boot版本:2.0.6.RELEASE
  
  IDE:IntelliJ IDEA 2018.2.4
  
  另外本文实验代码置于文尾,需要自取。
  
  设计用户和角色
  
  本文实验为了简化考虑,准备做如下设计:
  
  设计一个最简角色表role,包括角色ID和角色名称
  
  角色表
  
  设计一个最简用户表user,包括用户ID,用户名,密码
  
  用户表
  
  再设计一个用户和角色一对多的关联表user_roles 一个用户可以拥有多个角色 用户-角色对应表
  
  创建 Spring Security和 JWT加持的 Web工程
  
  pom.xml 中引入 Spring Security和 JWT所必需的依赖
  
  <dependency>
  
  <groupId>org.springframework.boot</groupId>
  
  <artifactId>spring-boot-starter-security</artifactId>
  
  </dependency>
  
  <dependency>
  
  <groupId>io.jsonwebtoken</groupId>
  
  <artifactId>jjwt</artifactId>
  
  <version>0.9.0</version>
  
  </dependency>
  
  项目配置文件中加入数据库和 JPA等需要的配置
  
  server.port=9991
  
  spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  
  spring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8
  
  spring.datasource.username=root
  
  spring.datasource.password=XXXXXX
  
  logging.level.org.springframework.security=info
  
  spring.jpa.hibernate.ddl-auto=update
  
  spring.jpa.show-sql=true
  
  spring.jackson.serialization.indent_output=true
  
  创建用户、角色实体
  
  用户实体 User:
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @Entity
  
  public class User implements UserDetails {
  
  @Id
  
  @GeneratedValue
  
  private Long id;
  
  private String username;
  
  private String password;
  
  @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
  
  private List<Role> roles;
  
  ...
  
  // 下面为实现UserDetails而需要的重写方法!
  
  @Override
  
  public Collection<? extends GrantedAuthority> getAuthorities() {
  
  List<GrantedAuthority> authorities = new ArrayList<>();
  
  for (Role role : roles) {
  
  authorities.add( new SimpleGrantedAuthority( role.getName() ) );
  
  }
  
  return authorities;
  
  }
  
  ...
  
  }
  
  此处所创建的 User类继承了 Spring Security的 UserDetails接口,从而成为了一个符合 Security安全的用户,即通过继承 UserDetails,即可实现 Security中相关的安全功能。
  
  角色实体 Role:
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @Entity
  
  public class Role {
  
  @Id
  
  @GeneratedValue
  
  private Long id;
  
  private String name;
  
  ... // 省略 getter和 setter
  
  }
  
  创建JWT工具类
  
  主要用于对 JWT Token进行各项操作,比如生成Token、验证Token、刷新Token等
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @Component
  
  public class JwtTokenUtil implements Serializable {
  
  private static final long serialVersionUID = -5625635588908941275L;
  
  private static final String CLAIM_KEY_USERNAME = "sub";
  
  private static final String CLAIM_KEY_CREATED = "created";
  
  public String generateToken(UserDetails userDetails) {
  
  ...
  
  }
  
  String generateToken(Map<String, Object> claims) {
  
  ...
  
  }
  
  public String refreshToken(String token) {
  
  ...
  
  }
  
  public Boolean validateToken(String token, UserDetails userDetails) {
  
  ...
  
  }
  
  ... // 省略部分工具函数
  
  }
  
  创建Token过滤器,用于每次外部对接口请求时的Token处理
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @Component
  
  public class JwtTokenFilter extends OncePerRequestFilter {
  
  @Autowired
  
  private UserDetailsService userDetailsService;
  
  @Autowired
  
  private JwtTokenUtil jwtTokenUtil;
  
  @Override
  
  protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
  
  String authHeader = request.getHeader( Const.HEADER_STRING );
  
  if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {
  
  final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() );
  
  String username = jwtTokenUtil.getUsernameFromToken(authToken);
  
  if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
  
  UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
  
  if (jwtTokenUtil.validateToken(authToken, userDetails)) {
  
  UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
  
  userDetails, null, userDetails.getAuthorities());
  
  authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
  
  request));
  
  SecurityContextHolder.getContext().setAuthentication(authentication);
  
  }
  
  }
  
  }
  
  chain.doFilter(request, response);
  
  }
  
  }
  
  Service业务编写
  
  主要包括用户登录和注册两个主要的业务
  
  public interface AuthService {
  
  User register( User userToAdd );
  
  String login( String username, String password );
  
  }
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @Service
  
  public class AuthServiceImpl implements AuthService {
  
  @Autowired
  
  private AuthenticationManager authenticationManager;
  
  @Autowired
  
  private UserDetailsService userDetailsService;
  
  @Autowired
  
  private JwtTokenUtil jwtTokenUtil;
  
  @Autowired
  
  private UserRepository userRepository;
  
  // 登录
  
  @Override
  
  public String login( String username, String password ) {
  
  UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
  
  final Authentication authentication = authenticationManager.authenticate(upToken);
  
  SecurityContextHolder.getContext().setAuthentication(authentication);
  
  final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
  
  final String token = jwtTokenUtil.generateToken(userDetails);
  
  return token;
  
  }
  
  // 注册
  
  @Override
  
  public User register( User userToAdd ) {
  
  final String username = userToAdd.getUsername();
  
  if( userRepository.findByUsername(username)!=null ) {
  
  return null;
  
  }
  
  BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  
  final String rawPassword = userToAdd.getPassword();
  
  userToAdd.setPassword( encoder.encode(rawPassword) );
  
  return userRepository.save(userToAdd);
  
  }
  
  }
  
  Spring Security配置类编写(非常重要)
  
  这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure配置,来实现用户自定义的部分。
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @Configuration
  
  @EnableWebSecurity
  
  @EnableGlobalMethodSecurity(prePostEnabled=true)
  
  public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  
  @Autowired
  
  private UserService userService;
  
  @Bean
  
  public JwtTokenFilter authenticationTokenFilterBean(www.zhenghongyule.com) throws Exception {
  
  return new JwtTokenFilter();
  
  }
  
  @Bean
  
  public AuthenticationManager authenticationManagerBean(www.hengtongyoule.com) throws Exception {
  
  return super.authenticationManagerBean(www.meiwanyule.cn/);
  
  }
  
  @Override
  
  protected void configure(www.jlzkyy.com AuthenticationManagerBuilder auth ) throws Exception {
  
  auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() );
  
  }
  
  @Override
  
  protected void configure( HttpSecurity httpSecurity ) throws Exception {
  
  httpSecurity.csrf().disable(www.gen-okamoto.com)
  
  .sessionManagement(www.xianjingshuiqi.com).sessionCreationPolicy(www.mren2yule.com/ SessionCreationPolicy.STATELESS).and()
  
http://dasheng178.com/#portal/list.html

  .authorizeRequests(www.yongshi123.cn)
  
  .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS请求全部放行
  
  .antMatchers(HttpMethod.POST, "/authentication/**www.mayiyule158.cn").permitAll(www.dfgjpt.com) //登录和注册的接口放行,其他接口全部接受验证
  
  .antMatchers(HttpMethod.www.ysgptvip.com POST).authenticated(www.jrgjze.com/)
  
  .antMatchers(HttpMethod.www.bsylept.com/ PUT).authenticated()
  
  .antMatchers(HttpMethod.DELETE).authenticated()
  
  .antMatchers(HttpMethod.GET).authenticated();
  
  // 使用前文自定义的 Token过滤器
  
  httpSecurity
  
  .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
  
  httpSecurity.headers().cacheControl();
  
  }
  
  }
  
  编写测试 Controller
  
  登录和注册的 Controller:
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @RestController
  
  public class JwtAuthController {
  
  @Autowired
  
  private AuthService authService;
  
  // 登录
  
  @RequestMapping(value = "/authentication/login", method = RequestMethod.POST)
  
  public String createToken( String username,String password ) throws AuthenticationException {
  
  return authService.login( username, password ); // 登录成功会返回JWT Token给用户
  
  }
  
  // 注册
  
  @RequestMapping(value = "/authentication/register", method = RequestMethod.POST)
  
  public User register( @RequestBody User addedUser ) throws AuthenticationException {
  
  return authService.register(addedUser);
  
  }
  
  }
  
  再编写一个测试权限的 Controller:
  
  /**
  
  * @ www.codesheep.cn
  
  * 20190312
  
  */
  
  @RestController
  
  public class TestController {
  
  // 测试普通权限
  
  @PreAuthorize("hasAuthority('ROLE_NORMAL')")
  
  @RequestMapping( value="/normal/test", method = RequestMethod.GET )
  
  public String test1() {
  
  return "ROLE_NORMAL /normal/test接口调用成功!";
  
  }
  
  // 测试管理员权限
  
  @PreAuthorize("hasAuthority('ROLE_ADMIN')")
  
  @RequestMapping( value = "/admin/test", method = RequestMethod.GET )
  
  public String test2() {
  
  return "ROLE_ADMIN /admin/test接口调用成功!";
  
  }
  
  }
  
  这里给出两个测试接口用于测试权限相关问题,其中接口 /normal/test需要用户具备普通角色(ROLE_NORMAL)即可访问,而接口/admin/test则需要用户具备管理员角色(ROLE_ADMIN)才可以访问。
  
  接下来启动工程,实验测试看看效果
  
  实验验证
  
  在文章开头我们即在用户表 user中插入了一条用户名为 codesheep的记录,并在用户-角色表 user_roles中给用户 codesheep分配了普通角色(ROLE_NORMAL)和管理员角色(ROLE_ADMIN)
  
  接下来进行用户登录,并获得后台向用户颁发的JWT Token
  
  用户登录并获得JWT Token
  
  接下来访问权限测试接口
  
  不带 Token直接访问需要普通角色(ROLE_NORMAL)的接口 /normal/test会直接提示访问不通:
  
  不带token访问是不通的
  
  而带 Token访问需要普通角色(ROLE_NORMAL)的接口 /normal/test才会调用成功:
  
  带token访问OK
  
  同理由于目前用户具备管理员角色,因此访问需要管理员角色(ROLE_ADMIN)的接口 /admin/test也能成功:
  
  访问需要管理员角色的接口OK
  
  接下里我们从用户-角色表里将用户codesheep的管理员权限删除掉,再访问接口 /admin/test,会发现由于没有权限,访问被拒绝了:
  
  由于权限不够而被拒绝
  
  经过一系列的实验过程,也达到了我们的预期!
  
  写在最后
  
  本文涉及的东西还是蛮多的,最后我们也将本文的实验源码放在 Github上,需要的可以自取:源码下载地址
  
  由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!
  
  My Personal Blog:CodeSheep 程序羊

基于Spring Security和 JWT的权限系统设计的更多相关文章

  1. 基于Spring Cloud、JWT 的微服务权限系统设计

    基于Spring Cloud.JWT 的微服务权限系统设计 https://gitee.com/log4j/pig https://github.com/kioyong/spring-cloud-de ...

  2. Spring Security和 JWT两大利器来打造一个简易的权限系统。

    写在前面 关于 Spring Security Web系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求.在Java EE领域,成熟的安全框架解决方案一般有 A ...

  3. 基于Spring Security 的JSaaS应用的权限管理

    1. 概述 权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源.资源包括访问的页面,访问的数据等,这在传统的应用系统中比较常见.本文介绍的则是基于Saas系统 ...

  4. 基于spring security 实现前后端分离项目权限控制

    前后端分离的项目,前端有菜单(menu),后端有API(backendApi),一个menu对应的页面有N个API接口来支持,本文介绍如何基于spring security实现前后端的同步权限控制. ...

  5. 用Spring Security, JWT, Vue实现一个前后端分离无状态认证Demo

    简介 完整代码 https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Deom 运行展示 后端 主要展示 Spring Security ...

  6. Spring Security整合JWT,实现单点登录,So Easy~!

    前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况 ...

  7. spring boot rest 接口集成 spring security(2) - JWT配置

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...

  8. Spring Security + OAuth2 + JWT 基本使用

    Spring Security + OAuth2 + JWT 基本使用 前面学习了 Spring Security 入门,现在搭配 oauth2 + JWT 进行测试. 1.什么是 OAuth2 OA ...

  9. 【Spring Cloud & Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权

    一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...

随机推荐

  1. HNOI2014做题笔记

    HNOI2014 世界树(虚树.倍增) \(\sum M \leq 3 \times 10^5\)虚树没得跑 对于所有重要点和它们的\(LCA\)建立虚树,然后计算出每一个虚树上的点被哪个重要点控制. ...

  2. [Oracle][DataGuard]Standby数据库文件有损坏时的处理方法

    需要参考: [Oracle]Recovering the primary database's datafile using the physical standby, and vice versa ...

  3. 聊聊Zookeeper应用场景、架构设计、选主机制

    Zookeeper作为一个分布式协调系统提供了一项基本服务:分布式锁服务,分布式锁是分布式协调技术实现的核心内容.像配置管理.任务分发.组服务.分布式消息队列.分布式通知/协调等,这些应用实际上都是基 ...

  4. Visual Studio 2019 Serial Keys

    Visual Studio 2019 Enterprise BF8Y8-GN2QH-T84XB-QVY3B-RC4DF Visual Studio 2019 Professional NYWVH-HT ...

  5. GlusterFS分布式存储系统中更换故障Brick的操作记录

    前面已经介绍了GlusterFS分布式存储集群环境部署记录,现在模拟下更换故障Brick的操作: 1)GlusterFS集群系统一共有4个节点,集群信息如下: 分别在各个节点上配置hosts.同步好系 ...

  6. ssh登陆服务器locale告警(-bash: warning: setlocale:)的处理方法

    使用ssh远程登陆 IDC机房服务器,发现老是出现如下告警信息: -bash: warning: setlocale: LC_CTYPE: cannot change locale (en_US.UT ...

  7. 分页查询信息(使用jdbc连接mysql数据库实现分页查询任务)

             分页查询信息       使用jdbc连接mysql数据库实现分页查询任务 通过mysql数据库提供的分页机制,实现商品信息的分页查询功能,将查询到的信息显示到jsp页面上. 本项目 ...

  8. 20135218 Linux 实践二 编译模块

    20135218 姬梦馨 1.编写模块代码 模块构造函数:执行insmod或modprobe指令加载内核模块时会调用的初始化函数.函数原型必须是module_init(),括号内是函数指针 模块析构函 ...

  9. 《Linux内核设计与实现》 第八周读书笔记 第四章 进程调度

    20135307 张嘉琪 第八周读书笔记 第四章 进程调度 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行态进程之间分配有限的处理器时间资源的内核子系统.只有 ...

  10. 实践——ELF文件格式分析

    一.分析文件头 1. 段入口类型定义(/usr/include/elf.h)下面产生的hello是32位的 使用命令#Hexdump –x ELF_1.o 第一行: 前4字节,蓝色部分,是一个魔数,表 ...