Recently, the company will rebuild some old projects, starting from the authentication architecture. Some of my experiences were written down. This article focuses on a set of simple designs written by closed development.
How to design a simple and universal architecture?
Authentication system, the most important two points, nothing more than, 1 login, 2 authentication. Everything else is centered around these two, and there’s some boundary condition processing. Like what about the supertube? What should I do if the user changes password or permission midway?
Start with the simplest database that doesn’t involve RBAC.
The user
Design a user table, including the user account, password, role and other information.
role
Since it is the simplest, use enumeration to write dead.
permissions
Access permissions are manually set for each interface based on roles.
What does the code look like?
Role of enumeration
public enum RoleType implements IntegerValueEnum {
ADMIN(1."Administrator"),
OPERATOR(2."Run"),
CUSTOMER_SERVICE(3."Customer service"),
MERCHANT(4."Merchants"),;private final Integer value;
private final String name;
RoleType(Integer value, String name) {
this.value = value;
this.name = name;
}
public static RoleType fromValue(final Integer value) {
return Stream.of(RoleType.values())
.filter(v -> v.getValue().equals(value))
.findFirst()
.orElseThrow(() -> new BizException("RoleType enumeration does not exist"));
}
public Integer getValue(a) {
return value;
}
public String getName(a) {
returnname; }}Copy the code
The user login
The password is encrypted by MD5, and the login token is generated by JWT. The first 6 bits of the password are added during the generation, so that the user can log in again after changing the password. If the user wants to change the role and needs to log in again, the user can imitate the design.
public PddToolsLoginRespDto login(String userName, String password) {
ThirdPlatformUser user = userMapper.selectByUsername(userName);
if (user == null) throw new BizException("User does not exist");
if(! DigestUtils.md5Hex(password).equals(user.getPassword())) {throw new BizException("Password error");
}
String token = getToken(user, DateUtil.localDateTime2Date(LocalDateTime.now().plusDays(EXPIRED_DURATION)));
PddToolsLoginRespDto loginRespDto = new PddToolsLoginRespDto();
BeanUtils.copyProperties(user, loginRespDto);
loginRespDto.setToken(token);
loginRespDto.setRouter(ROLE_ROUTER_MAP.get(user.getRoleType()));
loginRespDto.setUserName(userName);
loginRespDto.setUhzUsername(user.getUhzUsername());
// Non-administrator displays username
if (userName.contains("admin")) {
loginRespDto.setZhhUsername("");
} else {
loginRespDto.setZhhUsername(merchantService.getZhhUserName(user.getMerchantId()));
}
return loginRespDto;
}
public String getToken(ThirdPlatformUser user, Date expiredTime) {
Map<String, Object> claims = new HashMap<>(1);
claims.put(TOKEN_USER_ID_KEY, user.getId());
claims.put(TOKEN_USER_PASSWORD_KEY, user.getPassword().substring(0.6));
return "Bearer " + Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(expiredTime)
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
Copy the code
Interface authentication
It’s a little bit more complicated here, what interfaces need what permissions? How to judge whether the user has permission? Design two annotations for an interface.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
@Documented
public @interface Auth {
Class<? extends Authorize> value();
}
public interface Authorize {
void handle(Method method);
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Inherited
@Documented
public @interface Ignore {}
Copy the code
@auth can be used on classes or methods, and what authentication is required for table name interfaces. Authorize, the realization of this interface represents the specific authentication code. @ignore, indicating that this method does not require authentication.
Like a normal login class.
@RestController
@API (tags = "Login management ")
@Auth(UserAuthorize.class)
public class LoginController {
@Resource
LoginService loginService;
@Resource
MerchantService merchantService;
@apiOperation (value = "login ")
@PostMapping("/login")
@Ignore
public Rs<PddToolsLoginRespDto> login(@RequestBody @Valid PddToolsLoginReqtDto dto) {
return Rs.success(loginService.login(dto));
}
@apiOperation (value = "login token")
@PostMapping("/token")
public Rs<String> token(a) {
returnRs.success(merchantService.getAuthorization(LocalThread.getCurrentUser().getMerchantId())); }}Copy the code
This class uses the @auth (UserAuthorize. Class) annotation, so every method in this class follows the method of UserAuthorize handle. However, if the @ignore annotation is used in the login method, this method will not be authenticated. The specific authentication logic is written in the UserAuthorize, and the above interception logic is written in the Controller interceptor.
@Component
@Aspect
public class RequestAspect {
@Pointcut("execution(* com.xx.controller.. *. * (..) )"
public void pointcut(a) {}@Before("pointcut()")
public void before(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class controller = methodSignature.getDeclaringType();
Method method = methodSignature.getMethod();
// Ignore annotations on methods do nothing
if (method.isAnnotationPresent(Ignore.class)) {
return;
}
Auth auth = null;
if (method.isAnnotationPresent(Auth.class)) {
auth = method.getDeclaredAnnotation(Auth.class);
} else if (controller.isAnnotationPresent(Auth.class)) {
auth = (Auth) controller.getDeclaredAnnotation(Auth.class);
}
if (auth == null) {
return; } Authorize authorize = ApplicationContextRegister.getApplicationContext().getBean(auth.value()); authorize.handle(method); }}@Component
public class UserAuthorize implements Authorize {
@Resource
private HttpServletRequest request;
@Resource
private UserService userService;
@Override
public void handle(Method method) {
String uri = request.getRequestURI();
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
throw new BizException(method.getName() + "Token is empty");
}
ThirdPlatformUser user = userService.getUserByToken(token);
if (user.getIfAvailable() == UserAvailableStatus.NOT_AVAILABLE) {
throw new BizException("User is disabled");
}
LocalThread.set("user", user); }}Copy the code
So far, we have not distinguished permissions. If you want to design whether an interface can be accessed by a role, there are two solutions. 1. 2. Set the interface in the specification, and handle it uniformly in the Handle method. The system recommended the second, convenient and easy, as long as it comes in accordance with the specification. If the specification states that the interface accessible to the role ends in English with the role name, add the following code to the Handle method.
if(! uri.endsWith(user.getRoleType().getName())) {throw new BizException("Insufficient authority");
}
Copy the code
In this way, a simple authentication design can be designed to meet the requirements in the shortest time. Make development easy caught in a myth, just think about cow force system, the higher the QPS, the stronger the performance is a good system, but this is just a good development in the eyes of a system, the leader’s eyes, as long as you can meet the requirements in the shortest possible time to give him at the same time, this is what he expects a good system, we must be a little flexible, this way is go wide the babies. From a technical point of view, this system does have a lot of optimization, next time with the iteration of the architecture, slowly make this into a unified authentication + single sign-on system available to all systems.