Feature security (#94)

This commit is contained in:
qianmoQ 2022-10-20 01:43:37 +08:00 committed by GitHub
commit 4a8ae1c92e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1150 additions and 107 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,12 @@ 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"),
USER_BAD_CREDENTIALS(4005, "The account or password is incorrect");
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,35 @@
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.authentication.BadCredentialsException;
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());
if (authException instanceof BadCredentialsException) {
response.getWriter().print(JSON.toJSON(Response.failure(ServiceState.USER_BAD_CREDENTIALS)));
}
else {
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,38 @@
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;
INSERT INTO `datacap`.`users` (`id`, `username`, `password`)
VALUES (1, 'admin', '$2a$10$dNGjVHzGLnI7MA50iiD8V.LFPPYetB/04eTiZm8ULaso/BaKV.RS.'),
(2, 'datacap', '$2a$10$o3h5jWxwNzxkWyP4wPXz4eoduNkQpF7eaCLStw2VYlTU2BUbed0Di');
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
);
INSERT INTO `datacap`.`user_roles` (`user_id`, `role_id`)
VALUES (1, 1),
(2, 2);

View File

@ -0,0 +1,3 @@
export default {
token: 'AuthToken'
}

View File

@ -1,27 +1,37 @@
import { ResponseModel } from "@/model/ResponseModel";
import { message } from "ant-design-vue";
import {ResponseModel} from "@/model/ResponseModel";
import {message} from "ant-design-vue";
import axios from 'axios';
import Common from "@/common/Common";
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
const configure = {
headers: {
'Content-Type': 'application/json'
},
cancelToken: undefined
}
export class HttpCommon
{
private configure;
export class HttpCommon {
constructor() {
constructor()
{
if (process.env.NODE_ENV === 'development' ||
window.location.hostname === 'localhost') {
axios.defaults.baseURL = 'http://localhost:9096';
} else {
}
else {
axios.defaults.baseURL = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
}
const auth = JSON.parse(localStorage.getItem(Common.token) || '{}');
this.configure = {
headers: {
'Content-Type': 'application/json',
'Authorization': auth.type + ' ' + auth.token
},
cancelToken: undefined,
params: undefined
}
}
handlerSuccessful(result: any): ResponseModel {
handlerSuccessful(result: any): ResponseModel
{
const data = result.data;
let message = data.message;
if (data.message instanceof Array) {
@ -40,7 +50,8 @@ export class HttpCommon {
return response;
}
handlerFailed(error: any): ResponseModel {
handlerFailed(error: any): ResponseModel
{
const response: ResponseModel = {
code: 0,
message: error.message,
@ -49,9 +60,11 @@ export class HttpCommon {
return response;
}
get(url: string, params?: any): Promise<ResponseModel> {
get(url: string, params?: any): Promise<ResponseModel>
{
return new Promise((resolve) => {
axios.get(url, { params: params })
this.configure.params = params;
axios.get(url, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -61,10 +74,11 @@ export class HttpCommon {
});
}
post(url: string, data = {}, cancelToken?: any): Promise<ResponseModel> {
post(url: string, data = {}, cancelToken?: any): Promise<ResponseModel>
{
return new Promise((resolve) => {
configure.cancelToken = cancelToken;
axios.post(url, data, configure)
this.configure.cancelToken = cancelToken;
axios.post(url, data, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -74,9 +88,10 @@ export class HttpCommon {
});
}
put(url: string, data = {}): Promise<ResponseModel> {
put(url: string, data = {}): Promise<ResponseModel>
{
return new Promise((resolve) => {
axios.put(url, data, configure)
axios.put(url, data, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -86,9 +101,10 @@ export class HttpCommon {
});
}
delete(url: string): Promise<ResponseModel> {
delete(url: string): Promise<ResponseModel>
{
return new Promise((resolve) => {
axios.delete(url)
axios.delete(url, this.configure)
.then(result => {
resolve(this.handlerSuccessful(result));
}, error => {
@ -98,7 +114,8 @@ export class HttpCommon {
});
}
getAxios() {
getAxios()
{
return axios;
}
}

View File

@ -1,11 +0,0 @@
const en = {
common: {
home: 'Home',
query: 'Query',
admin: 'Admin',
source: 'Source',
history: 'History'
}
}
export default en;

View File

@ -0,0 +1,14 @@
export default {
home: 'Home',
query: 'Query',
admin: 'Admin',
source: 'DataSource',
history: 'History',
username: 'Username',
password: 'Password',
login: 'Login',
logout: 'Logout',
english: 'English',
chinese: 'Chinese',
register: 'Register'
}

View File

@ -0,0 +1,9 @@
import enGB from 'ant-design-vue/es/locale/en_GB'
import common from "@/i18n/langs/en/common";
import required from "@/i18n/langs/en/required";
export default {
...enGB,
common: common,
required: required
}

View File

@ -0,0 +1,4 @@
export default {
username: 'Please input your username!',
password: 'Please input your password!'
}

View File

@ -1,15 +1,7 @@
import en from "@/i18n/langs/en";
import zhCn from "@/i18n/langs/zhCn";
import enGB from 'ant-design-vue/es/locale/en_GB';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import en from "@/i18n/langs/en/index";
import zhCn from "@/i18n/langs/zhCn/index";
export default {
en: {
...en,
...enGB,
},
zh_cn: {
...zhCn,
...zhCN,
}
en: en,
zh_cn: zhCn
}

View File

@ -1,11 +0,0 @@
const zhCn = {
common: {
home: '主页',
query: '查询',
admin: '管理',
source: '源',
history: '历史'
}
}
export default zhCn;

View File

@ -0,0 +1,14 @@
export default {
home: '主页',
query: '查询',
admin: '管理',
source: '数据源',
history: '历史',
username: '用户名',
password: '密码',
login: '登录',
logout: '注销',
english: '英语',
chinese: '中文',
register: '注册'
}

View File

@ -0,0 +1,9 @@
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import common from "@/i18n/langs/zhCn/common";
import required from "@/i18n/langs/zhCn/required";
export default {
...zhCN,
common: common,
required: required
}

View File

@ -0,0 +1,4 @@
export default {
username: '请输入用户名!',
password: '请输入密码!'
}

View File

@ -0,0 +1,8 @@
export interface AuthResponse
{
token: string;
type: string;
id: number;
username: string;
roles: [];
}

View File

@ -0,0 +1,8 @@
export interface AuthUser
{
username: string;
password: string;
// Marks the error message returned after an operation
message?: string;
loading?: boolean;
}

View File

@ -1,8 +1,9 @@
import LayoutContainer from "@/views/layout/Layout.vue";
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import {createRouter, createWebHashHistory, RouteRecordRaw} from "vue-router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import Common from "@/common/Common";
NProgress.configure({
easing: 'ease',
@ -25,6 +26,9 @@ const routes: Array<RouteRecordRaw> = [
name: "dashboard",
redirect: "/dashboard/index",
component: LayoutContainer,
meta: {
requireAuth: true,
},
children: [
{
path: "index",
@ -37,6 +41,9 @@ const routes: Array<RouteRecordRaw> = [
name: "console",
redirect: "/console/index",
component: LayoutContainer,
meta: {
requireAuth: true,
},
children: [
{
path: "index",
@ -48,6 +55,9 @@ const routes: Array<RouteRecordRaw> = [
path: "/admin",
name: "admin",
component: LayoutContainer,
meta: {
requireAuth: true,
},
children: [
{
path: "source",
@ -69,6 +79,22 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("../views/common/NotFound.vue")
}
]
},
{
path: "/auth",
name: "auth",
children: [
{
name: "signin",
path: "signin",
component: () => import("../views/pages/auth/AuthSignin.vue")
},
{
name: "signup",
path: "signup",
component: () => import("../views/pages/auth/AuthSignup.vue")
}
]
}
];
@ -77,13 +103,30 @@ const router = createRouter({
routes
});
const authRouterWith = '/auth/sign';
router.beforeEach((to, from, next) => {
NProgress.start();
if (to.matched.length === 0) {
next({ name: "routerNotFound" })
next({name: "routerNotFound"})
}
else {
next();
if (to.meta.requireAuth) {
if (localStorage.getItem(Common.token)) {
next();
}
else {
next('/auth/signin');
}
}
else {
if (localStorage.getItem(Common.token) && to.path.startsWith(authRouterWith)) {
next('/');
}
else {
next();
}
}
}
})

View File

@ -0,0 +1,18 @@
import {ResponseModel} from "@/model/ResponseModel";
import {HttpCommon} from "@/common/HttpCommon";
import {AuthUser} from "@/model/AuthUser";
const defaultAuth = "/api/auth";
export class AuthService
{
signin(configure: AuthUser): Promise<ResponseModel>
{
return new HttpCommon().post(defaultAuth + '/signin', configure);
}
signup(configure: AuthUser): Promise<ResponseModel>
{
return new HttpCommon().post(defaultAuth + '/signup', configure);
}
}

View File

@ -1,62 +1,100 @@
<template>
<a-menu theme="dark" mode="horizontal" :style="{ lineHeight: '64px' }">
<a-menu-item>DataCap(incubator)</a-menu-item>
<a-menu-item key="dashboard">
<router-link to="/">
<home-filled/>
{{ $t('common.home') }}
</router-link>
</a-menu-item>
<a-menu-item key="console">
<router-link to="/console/index">
<console-sql-outlined/>
{{ $t('common.query') }}
</router-link>
</a-menu-item>
<a-sub-menu key="admin">
<template #icon>
<setting-outlined/>
</template>
<template #title>{{ $t('common.admin') }}</template>
<a-menu-item key="admin_source">
<router-link to="/admin/source">
<aim-outlined/>
{{ $t('common.source') }}
<div :style="{ clear: 'both' }">
<a-menu theme="dark" mode="horizontal" :style="{lineHeight: '64px', float: 'left', width: '60%'}">
<a-menu-item>DataCap(incubator)</a-menu-item>
<a-menu-item key="dashboard">
<router-link to="/">
<home-filled/>
{{ $t('common.home') }}
</router-link>
</a-menu-item>
<a-menu-item key="admin_history">
<router-link to="/admin/history">
<history-outlined/>
{{ $t('common.history') }}
<a-menu-item key="console">
<router-link to="/console/index">
<console-sql-outlined/>
{{ $t('common.query') }}
</router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="language">
<template #icon>
<a-dropdown placement="bottom">
<template #overlay>
<a-menu>
<a-menu-item key="en" @click="handlerChangeLang('en')">
English
</a-menu-item>
<a-menu-item key="zhCN" @click="handlerChangeLang('zh_cn')">
中文
</a-menu-item>
</a-menu>
</template>
<translation-outlined/>
</a-dropdown>
</template>
</a-sub-menu>
</a-menu>
<a-sub-menu key="admin">
<template #icon>
<setting-outlined/>
</template>
<template #title>{{ $t('common.admin') }}</template>
<a-menu-item key="admin_source">
<router-link to="/admin/source">
<aim-outlined/>
{{ $t('common.source') }}
</router-link>
</a-menu-item>
<a-menu-item key="admin_history">
<router-link to="/admin/history">
<history-outlined/>
{{ $t('common.history') }}
</router-link>
</a-menu-item>
</a-sub-menu>
</a-menu>
<a-menu theme="dark" mode="horizontal" :style="{lineHeight: '64px', float: 'right'}">
<a-sub-menu key="language">
<template #icon>
<a-dropdown placement="bottom">
<template #overlay>
<a-menu style="margin-top: 5px;">
<a-menu-item key="en" @click="handlerChangeLang('en')">
{{ $t('common.english') }}
</a-menu-item>
<a-menu-item key="zhCN" @click="handlerChangeLang('zh_cn')">
{{ $t('common.chinese') }}
</a-menu-item>
</a-menu>
</template>
<translation-outlined/>
</a-dropdown>
</template>
</a-sub-menu>
<a-sub-menu>
<template #icon>
<a-dropdown placement="bottomRight">
<template #overlay>
<a-menu style="margin-top: 5px;">
<a-menu-item @click="handlerLogout">
{{ $t('common.logout') }}
</a-menu-item>
</a-menu>
</template>
<a-avatar style="background-color: #87d068">{{ username }}</a-avatar>
</a-dropdown>
</template>
</a-sub-menu>
</a-menu>
</div>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import {AimOutlined, HomeFilled, SettingOutlined} from '@ant-design/icons-vue';
import Common from "@/common/Common";
import {AuthResponse} from "@/model/AuthResponse";
import router from "@/router";
export default defineComponent({
name: "LayoutHeader",
setup()
{
let username;
const authUser = JSON.parse(localStorage.getItem(Common.token) || '{}') as AuthResponse;
if (authUser) {
username = authUser.username;
}
const handlerLogout = () => {
localStorage.removeItem(Common.token);
router.push('/auth/signin')
}
return {
username,
handlerLogout
}
},
components: {HomeFilled, SettingOutlined, AimOutlined},
computed: {},
methods: {

View File

@ -0,0 +1,82 @@
<template>
<div class="main">
<a-row :gutter="[8,8]">
<a-col :span="8"/>
<a-col :span="8">
<a-result :title="'DataCap ' + $t('common.login')">
<template #icon>
<smile-twoTone/>
</template>
<template #extra>
<a-alert v-if="formState.message" :message="formState.message" type="error" show-icon style="margin-bottom: 10px;"/>
<a-form :model="formState" name="basic" :label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }" @finish="handlerAuthSignin">
<a-form-item :label="$t('common.username')" name="username"
:rules="[{ required: true, message: $t('required.username') }]">
<a-input v-model:value="formState.username"/>
</a-form-item>
<a-form-item :label="$t('common.password')" name="password"
:rules="[{ required: true, message: $t('required.password') }]">
<a-input-password v-model:value="formState.password"/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 6, span: 16 }">
<a-button :disabled="disabled" :loading="formState.loading" type="primary" html-type="submit">{{ $t('common.login') }}</a-button>
<a-button type="dashed" style="margin-left: 10px;" @click="handlerGoSignup">{{ $t('common.register') }}</a-button>
</a-form-item>
</a-form>
</template>
</a-result>
</a-col>
<a-col :span="8"/>
</a-row>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, reactive} from 'vue';
import {AuthUser} from "@/model/AuthUser";
import {AuthService} from "@/services/AuthService";
import Common from "@/common/Common";
import router from "@/router";
export default defineComponent({
setup()
{
const formState = reactive<AuthUser>({
username: '',
password: ''
});
const disabled = computed(() => {
return !(formState.username && formState.password);
});
const handlerAuthSignin = (values: any) => {
formState.loading = true;
new AuthService().signin(values)
.then(response => {
if (response.status) {
localStorage.setItem(Common.token, JSON.stringify(response.data));
router.push('/');
}
else {
formState.message = response.message;
}
})
.finally(() => {
formState.loading = undefined;
});
};
const handlerGoSignup = () => {
router.push('/auth/signup');
}
return {
formState,
disabled,
handlerAuthSignin,
handlerGoSignup
};
},
});
</script>

View File

@ -0,0 +1,83 @@
<template>
<div class="main">
<a-row :gutter="[8,8]">
<a-col :span="8"/>
<a-col :span="8">
<a-result :title="'DataCap ' + $t('common.register')">
<template #icon>
<smile-twoTone/>
</template>
<template #extra>
<a-alert v-if="formState.message" :message="formState.message" type="error" show-icon style="margin-bottom: 10px;"/>
<a-form :model="formState" name="basic" :label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }" @finish="handlerAuthSignup">
<a-form-item :label="$t('common.username')" name="username"
:rules="[{ required: true, message: $t('required.username') }]">
<a-input v-model:value="formState.username"/>
</a-form-item>
<a-form-item :label="$t('common.password')" name="password"
:rules="[{ required: true, message: $t('required.password') }]">
<a-input-password v-model:value="formState.password"/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 6, span: 16 }">
<a-button :disabled="disabled" :loading="formState.loading" type="primary" html-type="submit">{{ $t('common.register') }}</a-button>
<a-button type="dashed" style="margin-left: 10px;" @click="handlerGoSignin">{{ $t('common.login') }}</a-button>
</a-form-item>
</a-form>
</template>
</a-result>
</a-col>
<a-col :span="8"/>
</a-row>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, reactive} from 'vue';
import {AuthUser} from "@/model/AuthUser";
import {AuthService} from "@/services/AuthService";
import Common from "@/common/Common";
import router from "@/router";
import {message} from "ant-design-vue";
export default defineComponent({
setup()
{
const formState = reactive<AuthUser>({
username: '',
password: ''
});
const disabled = computed(() => {
return !(formState.username && formState.password);
});
const handlerAuthSignup = (values: any) => {
formState.loading = true;
new AuthService().signup(values)
.then(response => {
if (response.status) {
message.success('Success');
router.push('/auth/signin');
}
else {
formState.message = response.message;
}
})
.finally(() => {
formState.loading = undefined;
});
};
const handlerGoSignin = () => {
router.push('/auth/signin');
}
return {
formState,
disabled,
handlerAuthSignup,
handlerGoSignin
};
},
});
</script>