Authentication and Authorization
authentication
The main thing to do to certify in the program is to find out who the visitor is and has he registered with our system? Is he a user in our system? If so, is this user blacklisted by us? This is what certification does. For example: To enter a scenic spot, you have to buy a ticket first. If you don't have a ticket in your hand, your Uncle/Aunt will stop you from going in. If you have a ticket in your hand, your Uncle will also check to see if your ticket was bought today and if it expires. If you have a ticket and everything is OK, you can go in. This is a certification process!
authorization
The primary role of authorization is to verify that you have access to a resource! We all know that in a MySQL database, root users can add, delete, and alter data in each database at will, but ordinary users may only have the right to view it, not the right to delete and modify it. There is a very common requirement in programs as well: User A can access and modify a page, while User B may only have the right to access this page, not modify it!
A brief introduction to Shiro and JWT
Shiro is a lightweight security framework that allows you to quickly implement rights management using annotations. He is primarily used for authorization.
JWT(JSON Web Token), consisting of a request header, a request body, and a signature, is mainly used for authentication.
An Overall Idea for Achieving Authentication and Authorization
Client accesses the server and the server authenticates the request, mainly including whether the username and password are correct. If the authentication is successful, a certificate token will be issued to the client. Later, the client should carry the token when it visits the server again. If it is not carried or the token is tampered with, the authentication will fail! If token authentication succeeds and the client accesses the resource, then the server will also authenticate if the user has the right to access the resource at this time. If this user does not have access to this resource, access will also fail!
Database Table Design
Authentication and validation of privileges use the RBAC model with a total of five tables, user, user_role, role, role_permission and permission,
Where user_role and role_permission is an intermediate table, and the user table and role represent a one-to-many relationship.
The role and permission tables are also one-to-many relationships.
code implementation
The first is the login code, which is the code used to authenticate. When the user authenticates successfully, a token is returned to the client. Clients with failed authentication will not be able to get token!
Sign in
@RestController @Slf4j public class LoginController { @Resource private UserService userService; @PostMapping("/login") public ResponseBean login(@RequestBody JSONObject requestJson) { log.info("User Login"); String username = requestJson.getString("username"); String password = requestJson.getString("password"); if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { throw new RuntimeException("User name and password cannot be empty!"); } // Find the user information from the database based on the user name UserDTO userDTO = userService.getByUseName(username); // Get salt String salt = userDTO.getSalt(); // Encrypt the original password (using username + salt as salt) String encodedPassword = ShiroKit.md5(password, username + salt); if (null != userDTO && userDTO.getPassword().equals(encodedPassword)) { return new ResponseBean(200, "Login success", JWTUtil.sign(username, encodedPassword)); } else { throw new UnauthorizedException("Wrong username or password!"); } } }
JWT generates token (some tool classes with validation)
public class JWTUtil { final static Logger logger = LogManager.getLogger(JWTUtil.class); /** * Expiration time 30 minutes */ private static final long EXPIRE_TIME = 30 * 60 * 1000; /** * Verify that token is correct * * @param token secret key * @param secret User's password * @return Is it correct */ public static boolean verify(String token, String username, String secret) { try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username", username) .build(); verifier.verify(token); return true; } catch (Exception exception) { return false; } } /** * Getting information from a token does not require secret decryption * * @return token User name included in */ public static String getUsername(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; } } /** * Generate signature, expire in 30 minutes * * @param username User name * @param secret User's password * @return Encrypted token */ public static String sign(String username, String secret) { try { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); //Use the user's own password as the encryption key Algorithm algorithm = Algorithm.HMAC256(secret); // Attached username information // Builder pattern String jwtString = JWT.create() .withClaim("username", username) .withExpiresAt(date) .sign(algorithm); logger.debug(String.format("JWT:%s", jwtString)); return jwtString; } catch (UnsupportedEncodingException e) { return null; } } }
Filter
Exclude login requests, and then other requests must go through the authentication process before requesting:
public class JWTFilter extends BasicHttpAuthenticationFilter { private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); /** * Determine if the user wants to log in. * true:Is to sign in * Just check if the header contains an Authorization field */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String authorization = req.getHeader("Authorization"); return authorization != null; } /** * */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String authorization = httpServletRequest.getHeader("Authorization"); JWTToken token = new JWTToken(authorization); // Submit to realm for login, if wrong he will throw an exception and be caught getSubject(request, response).login(token); // Logon success if no exception is thrown, returning true return true; } /** * Here we detail why all of the eventual returns are true, that is, access is allowed * For example, we provide an address GET/article * Logged-in users and tourists see different things * If false is returned here, the request is directly intercepted and the user cannot see anything * So we're going to return true here, and in Controller you can tell if the user is logged in by subject.isAuthenticated(). * If some resources are accessible only to the logged-in user, we just need to add the @RequiresAuthentication comment to the method * However, one drawback of this approach is that it is not possible to filter authentication separately for requests such as GET,POST, etc. (because we override the official method), but it actually has little impact on the application. */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginAttempt(request, response)) { try { executeLogin(request, response); } catch (Exception e) { response401(request, response); } } return true; } /** * Provide support across domains */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // Cross-domain will first send an option request, where we directly return the option request to its normal state if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } /** * Jump illegal request to/401 */ private void response401(ServletRequest req, ServletResponse resp) { try { HttpServletResponse httpServletResponse = (HttpServletResponse) resp; httpServletResponse.sendRedirect("/401"); } catch (IOException e) { LOGGER.error(e.getMessage()); } } }
Customize MyRealm
Customize permissions and authentication logic:
@Configuration public class MyRealm extends AuthorizingRealm { @Resource private UserService userService; /** * This method must be overridden or Shiro will fail */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; } /** * This method is invoked only when it is necessary to detect user permissions, such as checkRole,checkPermission */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = JWTUtil.getUsername(principals.toString()); // Get user role privilege information based on user name UserDTO user = userService.getByUseName(username); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // Get Roles List<String> roleNames = getRoleNameList(user.getRoleList()); for (String roleName : roleNames) { simpleAuthorizationInfo.addRole(roleName); } // Get permissions List<String> permissions = getPermissionList(user.getPermissionList()); simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions)); return simpleAuthorizationInfo; } /** * Get permissions * * @param permissionList * @return */ private List<String> getPermissionList(List<Permission> permissionList) { List<String> permissions = new ArrayList<>(permissionList.size()); for (Permission permission : permissionList) { if (StringUtils.isNotBlank(permission.getPerCode())) { permissions.add(permission.getPerCode()); } } return permissions; } /** * Get Role Name * * @param roleList * @return */ private List<String> getRoleNameList(List<Role> roleList) { List<String> roleNames = new ArrayList<>(roleList.size()); for (Role role : roleList) { roleNames.add(role.getName()); } return roleNames; } /** * By default, this method is used to verify whether the user name is correct or not, and the error throws an exception. */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); // Decrypt to get username for comparison with database String username = JWTUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token invalid"); } UserDTO userBean = userService.getByUseName(username); if (userBean == null) { throw new AuthenticationException("User didn't existed!"); } if (!JWTUtil.verify(token, username, userBean.getPassword())) { throw new AuthenticationException("Username or password error"); } return new SimpleAuthenticationInfo(token, token, "my_realm"); } }
Interpretation of MyRealm
Customizing Realm is the key to achieving permissions. MyRealm inherits the AuthorizingRealm abstract class of the Shiro Framework and then overrides the doGetAuthorizationInfo method, in which we first take the user name from the token carried by the client, and then find out in the database the roles and permissions that the user has. Roles and privileges are then placed in SimpleAuthorizationInfo objects, respectively. The following:
// Get Roles List<String> roleNames = getRoleNameList(user.getRoleList()); for (String roleName : roleNames) { simpleAuthorizationInfo.addRole(roleName); } // Get permissions List<String> permissions = getPermissionList(user.getPermissionList()); simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));
Shiro Notes
1.RequiresAuthentication:
The class, instance, and method labeled with this annotation must be authenticated in the current session when accessed or invoked.
2.RequiresGuest:
The classes, instances, and methods labeled with this annotation can be "gust" when accessed or invoked without requiring authentication or having records in the original session.
3.RequiresPermissions:
The method labeled by this annotation can only be executed if the current Subject requires certain privileges. If the current Subject does not have this permission, the method will not be executed.
4.RequiresRoles:
The current Subject must have all the specified roles to access the methods labeled by the annotation. If the Subject does not have all the specified roles at the same time on that day, the method will not execute and an AuthorizationException exception will be thrown.
5.RequiresUser:
The current Subject must be the user of the application in order to access or invoke the classes, instances, and methods labeled by the annotation.
RequiresRoles and RequiresPermissions are commonly used.
These notes are in order of use:
RequiresRoles-->RequiresPermissions-->RequiresAuthentication-->RequiresUser-->RequiresGuest If there is more than one comment, the previous pass will continue to check the latter, otherwise return directly @RequiresRoles(value = {"admin","guest"},logical = Logical.OR) The purpose of this note is to carry token The role in must be admin,guest One of If the logical = Logical.OR Change to logical = Logical.AND,Then the effect of this comment becomes that the role must include both admin and guest Yes. @RequiresPermissions(logical = Logical.OR, value = {"user:view", "user:edit"}) and@RequiresRoles Similarly, the value of the permission is placed in value In, the relationship between values is used Logical.OR and Logical.AND control
Code Testing
Sign in
Access resources without token
Carry token access
token requiring guest role but admin
Guest privilege guest token
Insufficient privileges
Permission to access
Text Link
Shiro&JWT Implements User Login and Rights Management