在我目前实现的拍卖赢博体育程序版本中,我实现了一个温和的安全特性。通过使用uuid而不是简单的整数来标识用户,我使恶意用户更难以在系统中造成损害。
例如,要在我们的系统中发布用户的配置文件,我们必须提供表单的URL
/用户/ < id > /概要文件
其中
这是一种相对较弱的安全策略,计算机安全社区将其称为通过模糊的安全策略。这种策略有一个非常大的缺陷:如果恶意用户获得了系统中另一个用户的UUID,他们可能会使用该信息造成很多麻烦。事实上,如果我们在系统设计时不小心,我们可能会意外地让系统向攻击者泄露那些uuid。
事实上,当前系统已经主动泄露了这些uuid。攻击者所要做的就是请求拍卖的出价清单。由于返回的Bid对象将包括出价用户的uuid,因此攻击者将可以随时访问一堆有效的uuid。
一个改进的安全系统应该利用一个更仔细的控制系统来规范对系统关键部分的访问。以下是该系统的关键方面:
由于计算机安全在大多数实际系统中都是一个重要的考虑因素,因此Spring Boot提供了一种通过Spring security模块实现这样一个安全系统的方法也就不足为奇了。
为了将Spring Security模块添加到我们的赢博体育程序中,我们首先在pom.xml文件中添加一个依赖项:
<依赖> < groupId > org.springframework。boot</groupId> <artifactId>spring-boot-start -security</artifactId> </dependency>
Spring Security模块非常复杂。这种复杂性的主要原因是,随着时间的推移,已经开发了多种机制来处理服务器赢博体育程序的身份验证。Spring Security需要足够灵活,以适应广泛的身份验证方法。在这些笔记中,我将限制自己讨论Spring安全性的一个非常具体的赢博体育:保护对Spring Boot REST服务器上方法的访问。
无论我们使用哪种安全机制,Spring security都要求我们以一种非常特定的方式构建代码。Spring Security将要求我们创建Authentication对象,作为特定用户已通过身份验证的证据。身份验证对象包含UserDetail对象,该对象携带有关所讨论的用户的附加信息。
当REST请求到达服务器时,它将首先通过一个称为过滤器链的特殊机制。我们将把代码插入检查请求是否需要身份验证的过滤器链中,如果需要,我们的代码将检查是否存在令牌来对请求进行身份验证。如果我们能够验证请求,我们将创建必要的Authentication对象,该对象将传递到控制器,控制器可以使用该对象获取有关发出请求的用户的有用信息。
我们的身份验证机制将基于安全令牌的使用。要启动这个过程,用户将通过向URL /users/login提交一个user对象来登录到我们的服务器。该特定方法不需要身份验证,因此请求将直接传递给控制器方法。该方法将根据数据库检查用户的用户名和密码。如果登录有效,该方法将通过在其响应体中返回安全令牌来进行响应。
如果用户随后想要访问需要身份验证的服务器上的资源,他们将被期望将该令牌与请求一起发送。为此,他们将在采用该表单的请求中插入一个HTTP头
授权:承载<令牌>
其中
我们仍然需要一种机制来实现安全令牌。我们将使用的机制是一种广泛使用的令牌类型,称为JSON web令牌,简称JWT。
JWT由主体和签名组成。正文包含主题、过期时间和一组可选的附加声明。我们将使用subject部分来存储登录用户的UUID。我们将不再提出任何额外的索赔。jwt有一个签名,旨在让我们确信令牌是合法的,并且它没有从其原始形式更改。具体来说,要为JWT创建签名,我们将使用一个库,该库将JWT的主体与签名算法中只有服务器知道的秘钥值结合在一起。该签名算法的设计是这样的:如果我们不知道密钥,或者以任何方式改变了主体,我们将无法计算出正确的签名。当JWT到达服务器时,我们将使用的库可以检查签名是否有效以及正文是否被更改。
用于创建和检查令牌的库是jjwt库。要使用它,我们需要在项目中添加一些额外的依赖项:
<依赖> < groupId > io。jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.5</version> </dependency> <dependency> <groupId>io。jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io。jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.5</version> <scope>runtime</scope> </dependency>
接下来,我们为项目创建一个Service类来处理生成和检查jwt:
@Service公共类JwtService {SecretKey key = Jwts.SIG.HS256.key().build();public boolean isValid(String token) {try {return Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload() .getExpiration() .after(new Date(System.currentTimeMillis()));} catch (Exception ex){}返回false;}公共String getSubject(String token){返回Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload() .getSubject();} public String makeJwt(String userid) {return Jwts.builder() .subject(userid) .issuedAt(new Date(System.currentTimeMillis()) .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 30)) .signWith(key) .compact();}}
此服务在每次赢博体育程序启动时生成自己的密钥。要在makeJwt()方法中创建JWT,它将使用其密钥对JWT进行签名。isValid()方法首先使用密钥验证签名。如果签名没问题,我们继续检查过期日期。如果令牌没有过期,我们声明JWT有效。
除了帮助我们验证用户之外,jwt还将携带有关用户的有用信息。当我们为成功登录的用户创建JWT时,我们将把该用户的UUID作为JWT的主题嵌入到JWT中。当JWT返回用于请求时,我们可以提取该主题以获得UUID。
下面是处理登录的控制器方法的更新代码:
@PostMapping("/login") public ResponseEntity<String> checkLogin(@RequestBody UserDTO用户){用户结果= us.findByNameAndPassword(user. getname (), user. getpassword ());if (result == null){返回ResponseEntity.status(HttpStatus.UNAUTHORIZED)。body(“无效的用户名或密码”);} String token = jwtService.makeJwt(result.getUserid().toString());返回ResponseEntity.ok () .body(令牌);}
这里我们使用JwtService从用户的id生成一个令牌。
当请求到达服务器时,它必须在到达适当的控制器方法的途中通过过滤器链。我们可以将自己的类插入到过滤器链中,以测试授权头中是否存在JWT。
下面是该类的代码:
@组件公共类JwtAuthFilter扩展OncePerRequestFilter {@Autowired私有JwtService;@Override保护无效doFilterInternal(HttpServletRequest请求,HttpServletResponse响应,FilterChain FilterChain)抛出ServletException, IOException{字符串authHeader = request. getheader(“授权”);字符串token = null;String userid = null;if(authHeader != null && authHeader. if)startsWith("Bearer ")){token = authHeader.substring(7);if(jwtService.isValid(token)) userid = jwtService.getSubject(token);} if(userid != null && SecurityContextHolder.getContext().getAuthentication() == null){AuctionUserDetails userDetails = new AuctionUserDetails(userid);UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails. getauthorities ());authenticationToken。setDetails(新WebAuthenticationDetailsSource () .buildDetails(请求));SecurityContextHolder.getContext () .setAuthentication (authenticationToken);} filterChain。doFilter(请求、响应);}}
该组件被设计为插入过滤器链中。它的特点是请求将通过一个方法。该方法将检查请求是否包含Authorization头,在该头中查找令牌,然后要求JwtService检查令牌。
如果请求通过了测试,我们将通过构造一个Authentication对象并将该对象插入Spring Security安全上下文来进行响应。Spring Security要求Authentication对象包含UserDetails对象。UserDetails对象是一个实现Spring Security UserDetails接口的对象。对于这个项目,我为此目的创建了自己的自定义类:
公共类AuctionUserDetails实现UserDetails{私有静态最终长serialVersionUID = 1L;private String userid;private List<GrantedAuthority>权威;public AuctionUserDetails(字符串id) {userid = id;权限= new ArrayList<GrantedAuthority ();当局。添加(新SimpleGrantedAuthority(“用户”));} @重写公共集合<?extends GrantedAuthority> getAuthorities(){返回授权;} @覆盖公共字符串getPassword(){返回null;} @覆盖公共字符串getUsername(){返回userid;} @覆盖公共布尔isAccountNonExpired(){返回true;} @覆盖公共布尔isAccountNonLocked(){返回true;} @覆盖公共布尔值isCredentialsNonExpired(){返回true;} @覆盖公共布尔isEnabled(){返回true;}}
期望UserDetails对象以某种方式标识用户,并且它们还应该能够返回授予该用户的权限列表。权限系统使我们能够为系统中的不同用户授予不同级别的特权。对于本例,我们将通过授予系统的赢博体育用户普通USER权限来保持简单。如果愿意,我们可以稍后再回来,为管理员构造一个单独的类,授予这些用户USER权限和ADMIN权限。这将使我们能够设置控制器方法仅供具有提升的ADMIN权限的用户访问。
现在我们有了要插入到过滤器链中的过滤器类,我们必须插入它。这是通过创建一个配置类来设置Spring Security安全配置来完成的:
@Configuration @EnableWebSecurity @EnableMethodSecurity公共类SecurityConfig {@Autowired JwtAuthFilter JwtAuthFilter;@Bean SecurityFilterChain SecurityFilterChain (HttpSecurity http)抛出异常{http .csrf(csrf -> csrf.disable()) . sessionmanagement (management . > management . sessioncreationpolicy (SessionCreationPolicy.STATELESS)) . authorizehttprequests (authorization -> authorizerequestmatchers (HttpMethod. >))。POST, "/users", "/users/login"). permitall () . requestmatchers (GET, "/auctions", "/auctions/{id}/bids").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);返回http.build ();}}
这里有两个关键方法调用:authorizeHttpRequests()和. addfilterbefore()。第二个步骤将我们的自定义过滤器插入到过滤器链中以检查jwt。第一种方法允许我们区分需要身份验证和不需要身份验证的请求。
我们将不需要对以下四种特定请求类型进行身份验证:
/用户
:这将设置一个新用户。/ /用户登录
:这是登录检查/拍卖
:我们希望任何人都可以查看拍卖列表,而无需登录。/拍卖/ < id > /投标
:我们希望允许任何人查看任何现场拍卖出价的当前列表。这两个方法调用的顺序在这里很重要。我们首先要给一些请求一个绕过安全检查的机会。那些确实需要身份验证的请求将继续到我们的自定义过滤器,该过滤器将只对那些在Authorization头中具有有效JWT的请求进行身份验证。如果请求需要身份验证,并且安全上下文在过滤器链的末端不包含有效的Authorization,服务器将以403 (forbidden error)拒绝该请求。
这种安全机制的一个很好的副作用是,它可以根据需要将Authentication对象交付给控制器方法。这里有一个例子。在之前版本的服务器中,我们允许用户向URL发布Shipping对象
/用户/ < id > /航运
其中
/用户/运输
然后把控制器方法改成这样:
@PostMapping("/shipping") public ResponseEntity<String> postShipping(Authentication Authentication,@RequestBody ShippingDTO shipping) {AuctionUserDetails details = (AuctionUserDetails) Authentication . getprincipal ();UUID id = UUID. fromstring (details.getUsername());try {us. savshipping (id,shipping);} catch(WrongUserException ex){返回ResponseEntity.status(HttpStatus.UNAUTHORIZED)。body(“无效的用户id”);}返回ResponseEntity.status(HttpStatus.CREATED)。身体(“航运保存。”);}
新版本的控制器方法添加了一个参数,我们可以使用该参数请求安全机制为我们提供自定义过滤器生成的Authentication对象。我们知道该Authentication对象包含一个AuctionUserDetails对象,该对象可以告诉我们发布请求的用户的UUID,因此信息不再需要出现在URL中。
如果我们想在Postman中测试我们的新系统,我们可以从发送一个登录请求开始。
要发送需要身份验证的请求,我们必须单击Postman窗口中的Auth选项卡,选择“Bearer Token”作为要使用的授权类型,然后将JWT粘贴到Token字段中。
有了授权,我们就可以成功地发送请求: