您的位置 首页 > 数码极客

【510dd】基于Spring Security和 JWT的权限系统设计

写在前面

spring Security web系统的身份认证和权限模块也被视为系统的基础架构,几乎所有internet服务都包含此要求。

在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两大利器来打造一个简易的权限系统。


    设计用户和角色

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

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

    角色表

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

    用户表

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

    用户-角色对应表


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

    • 中引入 Spring Security和 JWT所必需的依赖
    <dependency> <groupId>org.;/groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
    • 项目配置文件中加入数据库和 jpa等需要的配置
    spring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8 logging.level.org.
    • 创建用户、角色实体

    用户实体 User:

    /** * @ www.code * 20190312 */ @Entity public class User implements UserDetails { @Id @GeneratedValue private Long id; private String username; private String password; @ManyToMany(cascade = {Ca},fetch = Fe) private List<Role> roles; ... // 下面为实现UserDetails而需要的重写方法! @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { au( new SimpleGrantedAuthority( role.getName() ) ); } return authorities; } ... }

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

    角色实体 Role:

    /** * @ www.code * 20190312 */ @Entity public class Role { @Id @GeneratedValue private Long id; private String name; ... // 省略 getter和 setter }
    • 创建JWT工具类

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

    /** * @ www.code * 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.code * 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 = reque( Con ); if (authHeader != null && au( Con )) { final String authToken = au( Con.length() ); String username = jw(authToken); if (username != null && Securi().getauthentication() == null) { UserDetails userDetails = (username); if (authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, u()); au(new WebAuthenticationDetailsSource().buildDetails( request)); Securi().setAuthentication(authentication); } } } c(request, response); } }
    • Service业务编写

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

    public interface AuthService { User register( User userToAdd ); String login( String username, String password ); } /** * @ www.code * 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 = au(upToken); Securi().setAuthentication(authentication); final UserDetails userDetails = u( username ); final String token = jw(userDetails); return token; } // 注册 @Override public User register( User userToAdd ) { final String username = u(); if( u(username)!=null ) { return null; } BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); final String rawPassword = u(); u( encoder.encode(rawPassword) ); return u(userToAdd); } }
    • Spring Security配置类编写(非常重要)

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

    /** * @ www.code * 20190312 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean public JwtTokenFilter authenticationTokenFilterBean() throws Exception { return new JwtTokenFilter(); } @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return (); } @Override protected void configure( AuthenticationManagerBuilder auth ) throws Exception { au( userService ).passwordEncoder( new BCryptPasswordEncoder() ); } @Override protected void configure( HttpSecurity httpSecurity ) throws Exception { ().disable() .sessionManagement().sessionCreationPolicy).and() .authorizeRequests() .antMatcher, "/**").permitAll() // OPTIONS请求全部放行 .antMatcher, "/authentication/**").permitAll() //登录和注册的接口放行,其他接口全部接受验证 .antMatcher).authenticated() .antMatcher).authenticated() .antMatcher).authenticated() .antMatcher).authenticated(); // 使用前文自定义的 Token过滤器 httpSecurity .addFilterBefore(authenticationTokenFilterBean(), U); ().cacheControl(); } }
    • 编写测试 Controller

    登录和注册的 Controller:

    /** * @ www.code * 20190312 */ @RestController public class JwtAuthController { @Autowired private AuthService authService; // 登录 @RequestMapping(value = "/authentication/login", method = Reque) public String createToken( String username,String password ) throws AuthenticationException { return au( username, password ); // 登录成功会返回JWT Token给用户 } // 注册 @RequestMapping(value = "/authentication/register", method = Reque) public User register( @RequestBody User addedUser ) throws AuthenticationException { return au(addedUser); } }

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

    /** * @ www.code * 20190312 */ @RestController public class TestController { // 测试普通权限 @PreAuthorize("hasAuthority('ROLE_NORMAL')") @RequestMapping( value="/normal/test", method = Reque ) public String test1() { return "ROLE_NORMAL /normal/test接口调用成功!"; } // 测试管理员权限 @PreAuthorize("hasAuthority('ROLE_ADMIN')") @RequestMapping( value = "/admin/test", method = Reque ) 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,会发现由于没有权限,访问被拒绝了:

    由于权限不够而被拒绝

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

    原文链接:

    关于作者: admin

    无忧经验小编鲁达,内容侵删请Email至wohenlihai#qq.com(#改为@)

    热门推荐