处理这种情况的最佳做法是有两个端点,每个端点都需要不同类型的权限:
- “/profiles/{profileId}/images”可供管理员使用,因此如果管理员想要更改其他用户的个人资料图片,他们可以通过调用此端点来实现。
- “/profiles/images”将负责更改具有最低权限的最通用用户。
因此,在这两种情况下,您都需要从 JWT 中提取 AccountId,并且您不应该直接从用户那里获取 AccountId,除非出于管理目的检查授权用户的权限。
现在,实现这样一个系统的最佳方法是使用 Spring Security 并创建自定义 AuthenticationToken,然后自定义 AbstractUserDetailsAuthenticationProvider、* AbstractAuthenticationProcessingFilter* 和 用户名密码验证令牌。
完成后,您可以将 Spring 配置为使用自定义提供程序进行身份验证。
用户名密码认证令牌
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
private Payload payload; // Payload can be any model class that encapsulates the payload of the JWT.
private boolean creationAllowed;
public JwtAuthenticationToken(String jwtToken) throws Exception {
super(null, jwtToken);
// Verify JWT and get the payload
this.payload = // set the payload
}
public JwtAuthenticationToken(String principal, JwtAuthenticationToken authToken, Collection<? extends GrantedAuthority> authorities) {
super(principal, authToken.getCredentials(), authorities);
this.payload = authToken.payload;
authToken.eraseCredentials(); // not sure if this is needed
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public Payload getPayload() {
return this.firebaseToken;
}
public boolean isCreationAllowed() {
return creationAllowed;
}
public void setCreationAllowed(boolean creationAllowed) {
this.creationAllowed = creationAllowed;
}
}
AbstractUserDetailsAuthenticationProvider
@Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
AppUserService appUserService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(JwtAuthenticationToken.class, authentication, () ->
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only JwtAuthenticationToken is supported")
);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) authentication;
String principal;
try {
principal = jwtAuthToken.getPayload().getEmail(); // Here I'm using email as the user identifier, this can be anything, for example AccountId
} catch (RuntimeException re) {
throw new AuthenticationException("Could not extract user's email address.");
}
AppUser user = (AppUser) this.retrieveUser(principal, jwtAuthToken);
return this.createSuccessAuthentication(principal, jwtAuthToken, user);
}
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
JwtAuthenticationToken result = new JwtAuthenticationToken((String) principal, (JwtAuthenticationToken) authentication, user.getAuthorities());
result.setDetails(user);
return result;
}
@Override
public UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
UserDetails userDetails = appUserService.loadUserByUsername(s);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) usernamePasswordAuthenticationToken;
if (userDetails != null)
return userDetails; // You need to create an UserDetails which will be set by the framework to the Security Context as the authenticated user, this will be useful later when you want to check the privileges.
else
throw new AuthenticationException("Creating the user details is not allowed.");
}
@Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
@Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
}
AbstractAuthenticationProcessingFilter
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
super("/**"); // The path that this filter needs to process, use "/**" to make sure all paths must be proessed.
}
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true; // Here I am returning true to require authentication for all requests.
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer "))
throw new AuthenticationException("No JWT token found in request headers");
String authToken = authorization.substring(7);
JwtAuthenticationToken token = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// Authentication process succeed, filtering the request in.
// As this authentication is in HTTP header, after success we need to continue the request normally
// and return the response as if the resource was not secured at all
chain.doFilter(request, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
// Authentication process failed, filtering the request out.
}
}
用户详情
public class AppUser implements UserDetails {
// A class to be used as a container for user details, you can add more details specific to your application here.
}
最后,你需要配置 Spring boot 来使用这些类:
安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
// -- public paths, for example: swagger ui paths
new AntPathRequestMatcher("/swagger-ui.html"),
new AntPathRequestMatcher("/swagger-resources/**"),
new AntPathRequestMatcher("/v2/api-docs"),
new AntPathRequestMatcher("/webjars/**")
);
private JwtAuthenticationProvider provider;
public SecurityConfig(JwtAuthenticationProvider provider) {
this.provider = provider;
}
@Override
public void configure(final WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS) // Allowing browser pre-flight
.requestMatchers(PUBLIC_URLS);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet authenticated
//.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.authenticationEntryPoint(forbiddenEntryPoint())
.and()
.authenticationProvider(this.provider)
.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
final JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
filter.setAuthenticationManager(this.authenticationManager());
filter.setAuthenticationSuccessHandler(this.successHandler());
filter.setAuthenticationFailureHandler(this.failureHandler());
return filter;
}
@Bean
JwtAuthenticationSuccessHandler successHandler() {
return new JwtAuthenticationSuccessHandler();
}
@Bean
JwtAuthenticationFailureHandler failureHandler() {
return new JwtAuthenticationFailureHandler();
}
/**
* Disable Spring boot automatic filter registration.
*/
@Bean
FilterRegistrationBean disableAutoRegistration(JwtAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
@Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
}
AuthenticationFailureHandler
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> data = new HashMap<>();
data.put("exception", e.getMessage());
httpServletResponse.getOutputStream().println(objectMapper.writeValueAsString(data));
}
}
AuthenticationSuccessHandler
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
}
}
好的!
现在您已经正确实现了安全性,您可以使用最后一部分从任何地方访问用户详细信息和权限:
用户详细信息服务
@Service
public class AppUserService implements UserDetailsService {
@Autowired
private AppUserRepository appUserRepository;
public AppUser getCurrentAppUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null)
return (AppUser) authentication.getDetails();
return null;
}
public String getCurrentPrincipal() {
return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<AppUser> appUserOptional = this.appUserRepository.findByEmailsContains(new EmailEntity(s)); // This should be changed in your case if you are using something like AccountId
appUserOptional.ifPresent(AppUser::loadAuthorities);
return appUserOptional.orElse(null);
}
}
太好了。
让我们看看如何在控制器中使用它:
@PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(@RequestParam("image") MultipartFile image) throws IOException {
AppUser user = this.appUserService.getCurrentAppUser();
Long id = user.getAccountId(); // Or profile id or any other identifier that you needed and extracted from the JWT after verification.
// set the profile picture.
// save changes of repository and return.
}
出于管理目的:
@PreAuthorize ("hasRole('ROLE_ADMIN')")
@PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
@PathVariable("profileId") UUID profileId, @RequestParam("image") MultipartFile image) throws IOException {
AppUser user = this.appUserService.getCurrentAppUser();
// set the profile picture using profileId parameter
// save changes of repository and return.
}
剩下的唯一任务是在从数据库加载ROLE_ADMIN 时将其分配给正确的用户。为此,有很多不同的方法,这完全取决于您的要求。总的来说,您可以将角色保存在数据库中并将其与特定用户相关联,然后使用实体简单地加载它。