Shiro & JWT implement user login and authority management

Before we start, let ’s understand two concepts

Authentication

The main thing to do in the authentication process is to figure out who the visitor is, has he registered in our system? Is he a user in our system? If yes, has this user been blacklisted by us! This is what certification does. For example: when you enter a tourist area, you must first buy a ticket. If you do n’t have a ticket at the ticket gate, you will be directly blocked by the uncle / aunt. Did you buy the ticket today? Has it expired ... If you have the ticket and everything is normal, then you can go in. This is a certification process!

Authorization

The role of authorization is to verify that you have the right to access a certain resource! We all know that in the MySQL database, the root user can add, delete, modify, and check the data of each library, but ordinary users may only have the right to view, but not the right to delete and modify. The same is true in the program, there is a very common requirement: user A can access and modify a page, for user B may only have the right to access this page, but can not be modified!


A brief introduction to shiro and jwt

jwt: JSON Web Token, consisting of three parts: request header, request body and signature. It is mainly used for authentication.
Shiro is a lightweight security framework that can use annotations to quickly implement permission management. He is mainly used for authorization.


The overall idea of ​​achieving authentication and authorization

The client accesses the server, and the server authenticates the request, including whether the user name and password are correct. If the authentication is successful, a credential token will be issued to the client, and the client will carry this token when accessing the server later. Or if the token is tampered with, authentication will fail! If the token authentication is successful and the client accesses the resource, then the server will also verify whether the user has the right to access this resource! If this user does not have the right to access this resource, the access will also fail!

Database table design

权限的认证和校验采用RBAC模型,总共有5张表,分别为user、user_role、role、role_permission和permission,
其中user_role和role_permission为中间表,user表和role表示一对多的关系,
role表和permission表也是一对多的关系。


Code

The first is the login code, which is the code used for authentication. When the user is successfully authenticated, a token will be returned to the client. If the authentication fails, the client will not be able to obtain the token!

log in

@RestController
@Slf4j
public class LoginController {
    @Resource
    private UserService userService;

    @PostMapping("/login")
    public ResponseBean login(@RequestBody JSONObject requestJson) {
        log.info("用户登录");
        String username = requestJson.getString("username");
        String password = requestJson.getString("password");

        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new RuntimeException("用户名和密码不可以为空!");
        }

        // 从数据库中根据用户名查找该用户信息
        UserDTO userDTO = userService.getByUseName(username);

        // 获取盐
        String salt = userDTO.getSalt();
        // 原密码加密(通过username + 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("用户名或者密码错误!");
        }
    }

}

JWT generates tokens (some tool classes with verification)

public class JWTUtil {
    final static Logger logger = LogManager.getLogger(JWTUtil.class);

    /**
     * 过期时间30分钟
     */
    private static final long EXPIRE_TIME = 30 * 60 * 1000;

    /**
     * 校验token是否正确
     *
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    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;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,30min后过期
     *
     * @param username 用户名
     * @param secret   用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            //使用用户自己的密码充当加密密钥
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            //  建造者模式
            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 the login request, and then all other requests must go through the authentication process before the request:

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**
     * 判断用户是否想要登入。
     * true:是要登录
     * 检测header里面是否包含Authorization字段即可
     */
    @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);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @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;
    }

    /**
     * 对跨域提供支持
     */
    @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"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /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

Customized permissions and identity authentication logic:

@Configuration
public class MyRealm extends AuthorizingRealm {
    @Resource
    private UserService userService;


    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        // 根据用户名获取用户角色 权限信息
        UserDTO user = userService.getByUseName(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 获取角色
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // 获取权限
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));
        return simpleAuthorizationInfo;
    }

    /**
     * 获取权限
     *
     * @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;
    }

    /**
     * 获取角色名称
     *
     * @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;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        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");
    }
}

MyRealm

Custom Realm is the key to achieving permissions. MyRealm inherits the Shiro framework ’s AuthorizingRealm abstract class, and then rewrites the doGetAuthorizationInfo method. In this method, we first take the username from the token carried by the client, and then check it in the database based on the username List the roles and permissions owned by the user, and then put the roles and permissions into the SimpleAuthorizationInfo object respectively. as follows:

 // 获取角色
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // 获取权限
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));

Shiro comment

1.RequiresAuthentication:

  • Classes, instances, and methods marked with this annotation must be authenticated in the current session when accessed or called.

2.RequiresGuest:

  • The class, instance, and method marked with this annotation when accessing or invoking, the current Subject can be a "gust" identity, there is no need to be authenticated or there is a record in the original session.

3.RequiresPermissions:

  • The current Subject needs to have certain specific permissions to execute the method marked by the annotation. If the current Subject does not have such authority, the method will not be executed.

4.RequiresRoles:

  • The current Subject must have all the specified roles in order to access the methods marked by the annotation. If the subject does not have all the specified roles at the same time, the method will not be executed and an AuthorizationException will be thrown.

5.RequiresUser:

  • The current subject must be the user of the application in order to access or call the classes, instances, and methods marked by the annotation.

We often use RequiresRoles and RequiresPermissions.
These annotations are used in order:

RequiresRoles-->RequiresPermissions-->RequiresAuthentication-->RequiresUser-->RequiresGuest
如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回

@RequiresRoles(value = {"admin","guest"},logical = Logical.OR)
   这个注解的作用是携带的token中的角色必须是admin、guest中的一个
   如果把logical = Logical.OR改成logical = Logical.AND,那么这个注解的作用就变成角色必须同时包含admin和guest了.
   
@RequiresPermissions(logical = Logical.OR, value = {"user:view", "user:edit"})
    和@RequiresRoles类似,权限的的值放在value中,值之间的关系用Logical.OR和Logical.AND控制

    

Code testing

log in

Login to get token

Access resources without carrying a token

Access resources without carrying a token

Access with token

Successful access with token

Requires guest role but is an admin token

Requires guest role but is an admin token

guest permission guest token

guest permission guest token

Insufficient permissions

Insufficient permissions

Have access

Have access

Project complete code

GitHub project complete code


Reference blog post

Published 4 original articles · praised 4 · visits 103

Guess you like

Origin blog.csdn.net/qq_40585384/article/details/105345435