文章共 1209字,阅读大约需要 5分钟,文尾有计时器可自行对时!


写在前面

  • 关于 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

注: 本文首发于 作者公众号 CodeSheep ,可 长按 扫描 下面的 小心心 来订阅 ↓ ↓ ↓


设计用户和角色

本文实验为了简化考虑,准备做如下设计:

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

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

  • 再设计一个用户和角色一对多的关联表 user_roles 一个用户可以拥有多个角色


创建Spring Security和JWT加持的Web工程

  • pom.xml 中引入 Spring Security和 JWT所必需的依赖

  1. <dependency>

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

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

  4. </dependency>


  5. <dependency>

  6.    <groupId>io.jsonwebtoken</groupId>

  7.    <artifactId>jjwt</artifactId>

  8.    <version>0.9.0</version>

  9. </dependency>

  • 项目配置文件中加入数据库和 JPA等需要的配置

  1. server.port=9991


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

  3. spring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8

  4. spring.datasource.username=root

  5. spring.datasource.password=XXXXXX


  6. logging.level.org.springframework.security=info


  7. spring.jpa.hibernate.ddl-auto=update

  8. spring.jpa.show-sql=true

  9. spring.jackson.serialization.indent_output=true

  • 创建用户、角色实体

用户实体 User

  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @Entity

  6. public class User implements UserDetails {


  7.    @Id

  8.    @GeneratedValue

  9.    private Long id;


  10.    private String username;


  11.    private String password;


  12.    @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)

  13.    private List<Role> roles;


  14.    ...


  15.    // 下面为实现UserDetails而需要的重写方法!

  16.    @Override

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

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

  19.        for (Role role : roles) {

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

  21.        }

  22.        return authorities;

  23.    }


  24.    ...

  25. }

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

角色实体 Role:

  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @Entity

  6. public class Role {


  7.    @Id

  8.    @GeneratedValue

  9.    private Long id;


  10.    private String name;


  11.    ... // 省略 getter和 setter

  12. }

  • 创建JWT工具类

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

  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @Component

  6. public class JwtTokenUtil implements Serializable {


  7.    private static final long serialVersionUID = -5625635588908941275L;


  8.    private static final String CLAIM_KEY_USERNAME = "sub";

  9.    private static final String CLAIM_KEY_CREATED = "created";


  10.    public String generateToken(UserDetails userDetails) {

  11.        ...

  12.    }


  13.    String generateToken(Map<String, Object> claims) {

  14.        ...

  15.    }


  16.    public String refreshToken(String token) {

  17.        ...

  18.    }


  19.    public Boolean validateToken(String token, UserDetails userDetails) {

  20.        ...

  21.    }


  22.    ... // 省略部分工具函数

  23. }

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

  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @Component

  6. public class JwtTokenFilter extends OncePerRequestFilter {


  7.    @Autowired

  8.    private UserDetailsService userDetailsService;


  9.    @Autowired

  10.    private JwtTokenUtil jwtTokenUtil;


  11.    @Override

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


  13.        String authHeader = request.getHeader( Const.HEADER_STRING );

  14.        if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {

  15.            final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() );

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

  17.            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

  18.                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

  19.                    if (jwtTokenUtil.validateToken(authToken, userDetails)) {

  20.                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(

  21.                                userDetails, null, userDetails.getAuthorities());

  22.                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(

  23.                                request));

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

  25.                    }

  26.            }

  27.        }

  28.        chain.doFilter(request, response);

  29.    }

  30. }

  • Service业务编写

主要包括用户登录和注册两个主要的业务

public interface AuthService {    User register( User userToAdd );    String login( String username, String password );}
  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @Service

  6. public class AuthServiceImpl implements AuthService {


  7.    @Autowired

  8.    private AuthenticationManager authenticationManager;


  9.    @Autowired

  10.    private UserDetailsService userDetailsService;


  11.    @Autowired

  12.    private JwtTokenUtil jwtTokenUtil;


  13.    @Autowired

  14.    private UserRepository userRepository;


  15.    // 登录

  16.    @Override

  17.    public String login( String username, String password ) {

  18.        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );

  19.        final Authentication authentication = authenticationManager.authenticate(upToken);

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

  21.        final UserDetails userDetails = userDetailsService.loadUserByUsername( username );

  22.        final String token = jwtTokenUtil.generateToken(userDetails);

  23.        return token;

  24.    }


  25.    // 注册

  26.    @Override

  27.    public User register( User userToAdd ) {

  28.        final String username = userToAdd.getUsername();

  29.        if( userRepository.findByUsername(username)!=null ) {

  30.            return null;

  31.        }

  32.        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

  33.        final String rawPassword = userToAdd.getPassword();

  34.        userToAdd.setPassword( encoder.encode(rawPassword) );

  35.        return userRepository.save(userToAdd);

  36.    }

  37. }

  • Spring Security配置类编写(非常重要

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

  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @Configuration

  6. @EnableWebSecurity

  7. @EnableGlobalMethodSecurity(prePostEnabled=true)

  8. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


  9.    @Autowired

  10.    private UserService userService;


  11.    @Bean

  12.    public JwtTokenFilter authenticationTokenFilterBean() throws Exception {

  13.        return new JwtTokenFilter();

  14.    }


  15.    @Bean

  16.    public AuthenticationManager authenticationManagerBean() throws Exception {

  17.        return super.authenticationManagerBean();

  18.    }


  19.    @Override

  20.    protected void configure( AuthenticationManagerBuilder auth ) throws Exception {

  21.        auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() );

  22.    }


  23.    @Override

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

  25.        httpSecurity.csrf().disable()

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

  27.                .authorizeRequests()

  28.                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // OPTIONS请求全部放行

  29.                .antMatchers(HttpMethod.POST, "/authentication/**").permitAll()  //登录和注册的接口放行,其他接口全部接受验证

  30.                .antMatchers(HttpMethod.POST).authenticated()

  31.                .antMatchers(HttpMethod.PUT).authenticated()

  32.                .antMatchers(HttpMethod.DELETE).authenticated()

  33.                .antMatchers(HttpMethod.GET).authenticated();


  34.        // 使用前文自定义的 Token过滤器

  35.        httpSecurity

  36.                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);


  37.        httpSecurity.headers().cacheControl();

  38.    }

  39. }

  • 编写测试 Controller

登录和注册的 Controller:

  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @RestController

  6. public class JwtAuthController {

  7.    @Autowired

  8.    private AuthService authService;


  9.    // 登录

  10.    @RequestMapping(value = "/authentication/login", method = RequestMethod.POST)

  11.    public String createToken( String username,String password ) throws AuthenticationException {

  12.        return authService.login( username, password ); // 登录成功会返回JWT Token给用户

  13.    }


  14.    // 注册

  15.    @RequestMapping(value = "/authentication/register", method = RequestMethod.POST)

  16.    public User register( @RequestBody User addedUser ) throws AuthenticationException {

  17.        return authService.register(addedUser);

  18.    }

  19. }

再编写一个测试权限的 Controller:

  1. /**

  2. * @ www.codesheep.cn

  3. * 20190312

  4. */

  5. @RestController

  6. public class TestController {


  7.    // 测试普通权限

  8.    @PreAuthorize("hasAuthority('ROLE_NORMAL')")

  9.    @RequestMapping( value="/normal/test", method = RequestMethod.GET )

  10.    public String test1() {

  11.        return "ROLE_NORMAL /normal/test接口调用成功!";

  12.    }


  13.    // 测试管理员权限

  14.    @PreAuthorize("hasAuthority('ROLE_ADMIN')")

  15.    @RequestMapping( value = "/admin/test", method = RequestMethod.GET )

  16.    public String test2() {

  17.        return "ROLE_ADMIN /admin/test接口调用成功!";

  18.    }

  19. }

这里给出两个测试接口用于测试权限相关问题,其中接口 /normal/test需要用户具备普通角色( ROLE_NORMAL)即可访问,而接口 /admin/test则需要用户具备管理员角色( ROLE_ADMIN)才可以访问。

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



实验验证

  • 在文章开头我们即在用户表 user中插入了一条用户名为 codesheep的记录,并在用户-角色表 user_roles中给用户 codesheep分配了普通角色( ROLE_NORMAL)和管理员角色( ROLE_ADMIN

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

  • 接下来访问权限测试接口

不带 Token直接访问需要普通角色( ROLE_NORMAL)的接口 /normal/test会直接提示访问不通:

而带 Token访问需要普通角色( ROLE_NORMAL)的接口 /normal/test才会调用成功:

同理由于目前用户具备管理员角色,因此访问需要管理员角色( ROLE_ADMIN)的接口 /admin/test也能成功:

接下里我们从用户-角色表里将用户 codesheep的管理员权限删除掉,再访问接口 /admin/test,会发现由于没有权限,访问被拒绝了:

经过一系列的实验过程,也达到了我们的预期!


写在最后

本文涉及的东西还是蛮多的,最后我们也将本文的实验源码放在 Github上,需要的可以自取:

github.com/hansonwang99/Spring-Boot-In-Action/tree/master/springbt_security_jwt

由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!

个人网站:www.codesheep.cn (程序羊)

我的更多系列原创文章在此:

  ●  程序羊的2018年终总(gen)结(feng)

  ●  利用K8S技术栈打造个人私有云系列连载文章

  ●  从一份配置清单详解Nginx服务器配置

  ●  Spring Boot Admin 2.0开箱体验

  ●  一文上手 Elasticsearch常用可视化管理工具

  ●  Docker容器可视化监控中心搭建

  ●  利用ELK搭建Docker容器化应用日志中心

  ●  RPC框架实践之:Google gRPC

  ●  一文详解 Linux系统常用监控工具


作者更多 务实、能看懂、可复现的 原创文章尽在公众号 CodeSheep,欢迎订阅 ⬇️⬇️⬇️


©著作权归作者所有:来自51CTO博客作者mb5fdcae83766b7的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. Debian设置允许root用户以ssh方式登录
  2. 给用户一个否减弱动画效果的选择[每日前端夜话0x8B]
  3. Azure DevTest Lab体验(二)用户测试
  4. Windows系统设置多用户同时远程登录
  5. windows10家庭版更改登录用户名
  6. 10年漫威,到底有多少角色。
  7. Pandas高端操作:10行代码解决用户游览日志合并排序问题
  8. centos 普通用户使用root的权限
  9. 做好付费预测,揭开用户转化的关键秘密

随机推荐

  1. 《PHP高级知识进阶》系列技术文章整理收
  2. [CI]CodeIgniter视图 & 模型 & 控制器
  3. 不使用PHP框架的原因?
  4. 如何在没有小部件工厂的情况下在Yii2中设
  5. Zend Framework 2:活动菜单项。
  6. 如何使用Javascript ajax发送网址? [重复]
  7. 解决Apache2+PHP上传文件大小限制的问题
  8. 如何在PHP 5.4.33 Apache服务器上启用mbs
  9. 解析错误:语法错误,E:\xampp\htdocs\on
  10. 访问控制允许原点 - 不工作