api: Add basic auth

This commit is contained in:
qianmoQ 2022-10-19 22:03:45 +08:00
parent 0bb286536b
commit 89f8e2fd55
20 changed files with 705 additions and 1 deletions

View File

@ -259,6 +259,7 @@
</tag>
</tags>
<additionalparam>-Xdoclint:none</additionalparam>
<failOnError>false</failOnError>
</configuration>
</plugin>
<plugin>

View File

@ -15,6 +15,7 @@
<properties>
<mysql.version>8.0.28</mysql.version>
<h2.version>2.1.214</h2.version>
<jjwt.version>0.9.1</jjwt.version>
<findbugs.version>3.0.1</findbugs.version>
<frontend-maven-plugin.version>1.12.1</frontend-maven-plugin.version>
<node.version>v16.10.0</node.version>
@ -45,6 +46,16 @@
<artifactId>spring-boot-starter-aop</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>

View File

@ -3,3 +3,6 @@ server.port=9096
spring.datasource.url=jdbc:mysql://localhost:3306/datacap?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=12345678
datacap.security.secret=DataCapSecretKey
datacap.security.expiration=86400000

View File

@ -0,0 +1,29 @@
package io.edurt.datacap.server.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.List;
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class JwtResponse
{
private String token;
private String type = "Bearer";
private Long id;
private String username;
private List<String> roles;
public JwtResponse(String accessToken, Long id, String username, List<String> roles)
{
this.token = accessToken;
this.id = id;
this.username = username;
this.roles = roles;
}
}

View File

@ -7,7 +7,11 @@ public enum ServiceState
PLUGIN_EXECUTE_FAILED(2002, "Plugin execute failed"),
REQUEST_VALID_ARGUMENT(3001, "The related parameters cannot be verified"),
REQUEST_VALID_ARGUMENT_FORMAT(3002, "Unable to format related parameters"),
REQUEST_VALID_ARGUMENT_LAYOUT(3003, "Related parameters cannot be resolved");
REQUEST_VALID_ARGUMENT_LAYOUT(3003, "Related parameters cannot be resolved"),
USER_NOT_FOUND(4001, "User dose not exists"),
USER_ROLE_NOT_FOUND(4002, "User role dose not exists"),
USER_UNAUTHORIZED(4003, "Insufficient current user permissions"),
USER_EXISTS(4004, "User exists");
private Integer code;
private String value;

View File

@ -0,0 +1,76 @@
package io.edurt.datacap.server.configure;
import io.edurt.datacap.server.security.AuthTokenFilterService;
import io.edurt.datacap.server.security.JwtAuthEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
// securedEnabled = true,
// jsr250Enabled = true,
prePostEnabled = true)
public class SecurityConfigure
extends WebSecurityConfigurerAdapter
{
private final UserDetailsService userDetailsService;
private final JwtAuthEntryPoint unauthorizedHandler;
public SecurityConfigure(UserDetailsService userDetailsService, JwtAuthEntryPoint unauthorizedHandler)
{
this.userDetailsService = userDetailsService;
this.unauthorizedHandler = unauthorizedHandler;
}
@Bean
public AuthTokenFilterService authenticationJwtTokenFilter()
{
return new AuthTokenFilterService();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder)
throws Exception
{
authenticationManagerBuilder.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean()
throws Exception
{
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http)
throws Exception
{
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}

View File

@ -0,0 +1,36 @@
package io.edurt.datacap.server.controller;
import io.edurt.datacap.server.common.JwtResponse;
import io.edurt.datacap.server.common.Response;
import io.edurt.datacap.server.entity.UserEntity;
import io.edurt.datacap.server.service.UserService;
import io.edurt.datacap.server.validation.ValidationGroup;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController
{
private final UserService userService;
public AuthController(UserService userService)
{
this.userService = userService;
}
@PostMapping("/signin")
public Response<JwtResponse> authenticateUser(@RequestBody @Validated(ValidationGroup.Crud.Auth.class) UserEntity configure)
{
return this.userService.authenticate(configure);
}
@PostMapping("/signup")
public Response<?> registerUser(@RequestBody @Validated(ValidationGroup.Crud.Create.class) UserEntity configure)
{
return this.userService.saveOrUpdate(configure);
}
}

View File

@ -0,0 +1,41 @@
package io.edurt.datacap.server.entity;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.sql.Timestamp;
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "role")
@org.hibernate.annotations.Table(appliesTo = "role", comment = "User rights configuration table")
@SuppressFBWarnings(value = {"EI_EXPOSE_REP"},
justification = "I prefer to suppress these FindBugs warnings")
public class RoleEntity
{
@Id()
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "description", columnDefinition = "varchar(1000)")
private String description;
@Column(name = "create_time", columnDefinition = "datetime(5) default CURRENT_TIMESTAMP()")
private Timestamp createTime;
}

View File

@ -0,0 +1,71 @@
package io.edurt.datacap.server.entity;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.edurt.datacap.server.validation.ValidationGroup;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.sql.Timestamp;
import java.util.HashSet;
import java.util.Set;
@Entity
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users",
uniqueConstraints = {
@UniqueConstraint(columnNames = "username")
})
@SuppressFBWarnings(value = {"EI_EXPOSE_REP"},
justification = "I prefer to suppress these FindBugs warnings")
public class UserEntity
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(groups = {
ValidationGroup.Crud.Create.class,
ValidationGroup.Crud.Update.class,
ValidationGroup.Crud.Auth.class
})
@Size(max = 20)
@Column(name = "username")
private String username;
@NotBlank(groups = {
ValidationGroup.Crud.Create.class,
ValidationGroup.Crud.Update.class,
ValidationGroup.Crud.Auth.class
})
@Size(max = 120)
@Column(name = "password")
private String password;
@Column(name = "create_time", columnDefinition = "datetime(5) default CURRENT_TIMESTAMP()")
private Timestamp createTime;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<RoleEntity> roles = new HashSet<>();
}

View File

@ -0,0 +1,12 @@
package io.edurt.datacap.server.repository;
import io.edurt.datacap.server.entity.RoleEntity;
import org.springframework.data.repository.PagingAndSortingRepository;
import java.util.Optional;
public interface RoleRepository
extends PagingAndSortingRepository<RoleEntity, Long>
{
Optional<RoleEntity> findByName(String name);
}

View File

@ -0,0 +1,12 @@
package io.edurt.datacap.server.repository;
import io.edurt.datacap.server.entity.UserEntity;
import org.springframework.data.repository.PagingAndSortingRepository;
import java.util.Optional;
public interface UserRepository
extends PagingAndSortingRepository<UserEntity, Long>
{
Optional<UserEntity> findByUsername(String username);
}

View File

@ -0,0 +1,60 @@
package io.edurt.datacap.server.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class AuthTokenFilterService
extends OncePerRequestFilter
{
@Autowired
private JwtService jwtService;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException
{
try {
String jwt = parseJwt(request);
if (jwt != null && jwtService.validateJwtToken(jwt)) {
String username = jwtService.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request)
{
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}

View File

@ -0,0 +1,29 @@
package io.edurt.datacap.server.security;
import io.edurt.datacap.server.common.JSON;
import io.edurt.datacap.server.common.Response;
import io.edurt.datacap.server.common.ServiceState;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component
public class JwtAuthEntryPoint
implements AuthenticationEntryPoint
{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)
throws IOException
{
log.error("Unauthorized error: {}", authException.getMessage());
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_UNAUTHORIZED)));
}
}

View File

@ -0,0 +1,69 @@
package io.edurt.datacap.server.security;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
public class JwtService
{
@Value("${datacap.security.secret}")
private String jwtSecret = "DataCapSecretKey";
@Value("${datacap.security.expiration}")
private int jwtExpirationMs = 86400000;
public String generateJwtToken(Authentication authentication)
{
UserDetailsService userPrincipal = (UserDetailsService) authentication.getPrincipal();
return Jwts.builder()
.setSubject((userPrincipal.getUsername()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token)
{
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateJwtToken(String authToken)
{
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
}
catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
}
catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
}
catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
}
catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
}
catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
}

View File

@ -0,0 +1,89 @@
package io.edurt.datacap.server.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.edurt.datacap.server.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class UserDetailsService
implements UserDetails
{
private Long id;
private String username;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserDetailsService(Long id, String username, String password,
Collection<? extends GrantedAuthority> authorities)
{
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsService build(UserEntity user)
{
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return new UserDetailsService(
user.getId(),
user.getUsername(),
user.getPassword(),
authorities);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
return authorities;
}
@Override
public String getPassword()
{
return password;
}
@Override
public String getUsername()
{
return username;
}
@Override
public boolean isAccountNonExpired()
{
return true;
}
@Override
public boolean isAccountNonLocked()
{
return true;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return true;
}
public Long getId()
{
return id;
}
}

View File

@ -0,0 +1,31 @@
package io.edurt.datacap.server.security;
import io.edurt.datacap.server.entity.UserEntity;
import io.edurt.datacap.server.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@Service
public class UserDetailsServiceImpl
implements org.springframework.security.core.userdetails.UserDetailsService
{
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository)
{
this.userRepository = userRepository;
}
@Override
@Transactional
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException
{
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsService.build(user);
}
}

View File

@ -0,0 +1,12 @@
package io.edurt.datacap.server.service;
import io.edurt.datacap.server.common.JwtResponse;
import io.edurt.datacap.server.common.Response;
import io.edurt.datacap.server.entity.UserEntity;
public interface UserService
{
Response<UserEntity> saveOrUpdate(UserEntity configure);
Response<JwtResponse> authenticate(UserEntity configure);
}

View File

@ -0,0 +1,86 @@
package io.edurt.datacap.server.service.impl;
import io.edurt.datacap.server.common.JwtResponse;
import io.edurt.datacap.server.common.Response;
import io.edurt.datacap.server.common.ServiceState;
import io.edurt.datacap.server.entity.RoleEntity;
import io.edurt.datacap.server.entity.UserEntity;
import io.edurt.datacap.server.repository.RoleRepository;
import io.edurt.datacap.server.repository.UserRepository;
import io.edurt.datacap.server.security.JwtService;
import io.edurt.datacap.server.security.UserDetailsService;
import io.edurt.datacap.server.service.UserService;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl
implements UserService
{
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder encoder;
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder encoder, AuthenticationManager authenticationManager, JwtService jwtService)
{
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.encoder = encoder;
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
}
@Override
public Response<UserEntity> saveOrUpdate(UserEntity configure)
{
Optional<UserEntity> userOptional = this.userRepository.findByUsername(configure.getUsername());
if (userOptional.isPresent()) {
return Response.failure(ServiceState.USER_EXISTS);
}
UserEntity user = new UserEntity();
user.setUsername(configure.getUsername());
user.setPassword(encoder.encode(configure.getPassword()));
Set<RoleEntity> userRoles = configure.getRoles();
Set<RoleEntity> roles = new HashSet<>();
if (ObjectUtils.isEmpty(userRoles)) {
Optional<RoleEntity> userRoleOptional = roleRepository.findByName("User");
if (!userRoleOptional.isPresent()) {
return Response.failure(ServiceState.USER_ROLE_NOT_FOUND);
}
roles.add(userRoleOptional.get());
}
user.setRoles(roles);
return Response.success(userRepository.save(user));
}
@Override
public Response<JwtResponse> authenticate(UserEntity configure)
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(configure.getUsername(), configure.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtService.generateJwtToken(authentication);
UserDetailsService userDetails = (UserDetailsService) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
return Response.success(new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(), roles));
}
}

View File

@ -23,5 +23,9 @@ public interface ValidationGroup
interface Delete
extends Crud
{}
interface Auth
extends Crud
{}
}
}

View File

@ -1,2 +1,30 @@
ALTER TABLE `datacap`.`source`
ADD COLUMN `_ssl` boolean default false;
CREATE TABLE IF NOT EXISTS `datacap`.`users`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL COMMENT ' ',
`password` varchar(255) DEFAULT NULL COMMENT ' ',
`create_time` datetime(5) DEFAULT CURRENT_TIMESTAMP(5),
PRIMARY KEY (`id`)
) DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `datacap`.`role`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL COMMENT ' ',
`description` varchar(255) DEFAULT NULL COMMENT ' ',
`create_time` datetime(5) DEFAULT CURRENT_TIMESTAMP(5),
PRIMARY KEY (`id`)
) DEFAULT CHARSET = utf8;
INSERT INTO `datacap`.`role`(`name`, `description`)
VALUES ('Admin', 'Admin role'),
('User', 'User role');
CREATE TABLE IF NOT EXISTS `datacap`.`user_roles`
(
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL
)