Shiro + JWT + Spring boot restful easy tutorial

Time:2021-4-20

characteristic

  • Shiro’s annotation configuration is fully used to maintain a high degree of flexibility.
  • Give up cookie and session, use JWT for authentication, and fully realize stateless authentication.
  • JWT key supports expiration time.
  • Support for cross domain.

preparation

Before you start this tutorial, make sure you are familiar with the following points.

  • Spring boot basic syntax, at least to understandControllerRestControllerAutowiredAnd so on. In fact, just look at the official getting start tutorial.
  • JWT(JSON web token) and can operate JWT easilyJAVA SDK
  • Shiro’s basic operation, take a look at the official10 Minute TutorialThat’s it.
  • Simulate HTTP request tool, I use postman.

Briefly explain why we use JWT, because we want to achieve complete separation of front and back ends, so it is impossible to use JWTsessioncookieSo JWT is used. You can use an encryption key to authenticate the front and back end.

Program logic

  1. We post user name and password to/loginIf the login is successful, an encrypted token will be returned. If the login fails, a 401 error will be returned directly.
  2. After that, the user’s request to access each URL that needs permission must be in theheaderAdd inAuthorizationFields, such asAuthorization: tokentokenIs the key.
  3. It’s going on backstagetokenIf there is a misunderstanding, return to 401 directly.

Token encryption description

  • YesusernameThe information is in the token.
  • The expiration time is set.
  • Use user login password pairtokenEncryption.

Token verification process

  1. gettokenCarried inusernameInformation.
  2. Search the database for the user and get his password.
  3. Use the user’s password to verifytokenIs it correct.

Preparing Maven files

Create a new Maven project and add related dependencies.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.inlighting</groupId>
    <artifactId>shiro-study</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>1.5.8.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
                <! -- srping boot packaging tool -- >
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>1.5.7.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <! -- specifies the JDK compiled version -- >
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Note that you specify the JDK version and encoding.

Building a simple data source

To reduce the code for the tutorial, I useHashMapA database is simulated locally, and the structure is as follows:

username password role permission
smith smith123 user view
danny danny123 admin view,edit

This is the simplest user permission table. If you want to know more about it, you can use Baidu RBAC.

Then build oneUserServiceTo simulate a database query and put the results in theUserBeanin

UserService.java

@Component
public class UserService {

    public UserBean getUser(String username) {
        //No such user returns null directly
        if (! DataSource.getData().containsKey(username))
            return null;

        UserBean user = new UserBean();
        Map<String, String> detail = DataSource.getData().get(username);

        user.setUsername(username);
        user.setPassword(detail.get("password"));
        user.setRole(detail.get("role"));
        user.setPermission(detail.get("permission"));
        return user;
    }
}

UserBean.java

public class UserBean {
    private String username;

    private String password;

    private String role;

    private String permission;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public String getPermission() {
        return permission;
    }

    public void setPermission(String permission) {
        this.permission = permission;
    }
}

Configure JWT

We write a simple JWT encryption and verification tool, and use the user’s own password as the encryption key, which ensures that the token cannot be cracked even if it is intercepted by others. And we’re heretokenIt comes withusernameAnd the key will expire in 5 minutes.

public class JWTUtil {

    //The expiration time is 5 minutes
    private static final long EXPIRE_TIME = 5*60*1000;

    /**
     *  Check whether the token is correct
     *@ param token key
     * @ Param secret the password of the user
     *Is @ return 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();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     *The information in the token can be obtained without secret decryption
     *User name contained in @ return token
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     *Generate a signature, which will expire in 5 minutes
     *@ param user name
     * @ Param secret the password of the user
     *@ return encrypted token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            //With user name information
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

Build URL

ResponseBean.java

Since we want to implement restful, we need to ensure that the format of each return is the same, so I set up aResponseBeanTo unify the return format.

public class ResponseBean {
    
    //HTTP status code
    private int code;

    //Return information
    private String msg;

    //Data returned
    private Object data;

    public ResponseBean(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

Custom exception

In order to realize that I can manually throw an exception, I wrote one myselfUnauthorizedException.java

public class UnauthorizedException extends RuntimeException {
    public UnauthorizedException(String msg) {
        super(msg);
    }

    public UnauthorizedException() {
        super();
    }
}

URL structure

URL effect
/login Login
/article Everyone can access it, but users and visitors see different content
/require_auth Only the logged in user can access it
/require_role Only the role user of admin can log in
/require_permission Only users with view and edit rights can access it

Controller

@RestController
public class WebController {

    private static final Logger LOGGER = LogManager.getLogger(WebController.class);

    private UserService userService;

    @Autowired
    public void setService(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseBean login(@RequestParam("username") String username,
                              @RequestParam("password") String password) {
        UserBean userBean = userService.getUser(username);
        if (userBean.getPassword().equals(password)) {
            return new ResponseBean(200, "Login success", JWTUtil.sign(username, password));
        } else {
            throw new UnauthorizedException();
        }
    }

    @GetMapping("/article")
    public ResponseBean article() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            return new ResponseBean(200, "You are already logged in", null);
        } else {
            return new ResponseBean(200, "You are guest", null);
        }
    }

    @GetMapping("/require_auth")
    @RequiresAuthentication
    public ResponseBean requireAuth() {
        return new ResponseBean(200, "You are authenticated", null);
    }

    @GetMapping("/require_role")
    @RequiresRoles("admin")
    public ResponseBean requireRole() {
        return new ResponseBean(200, "You are visiting require_role", null);
    }

    @GetMapping("/require_permission")
    @RequiresPermissions(logical = Logical.AND, value = {"view", "edit"})
    public ResponseBean requirePermission() {
        return new ResponseBean(200, "You are visiting permission require edit,view", null);
    }

    @RequestMapping(path = "/401")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResponseBean unauthorized() {
        return new ResponseBean(401, "Unauthorized", null);
    }
}

Handling frame exceptions

As I said before, restful should unify the return format, so we also need to deal with it globallySpring BootException thrown by. utilize@RestControllerAdviceIt can be well realized.

@RestControllerAdvice
public class ExceptionController {

    //Catch Shiro's exception
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public ResponseBean handle401(ShiroException e) {
        return new ResponseBean(401, e.getMessage(), null);
    }

    //Capture unauthorized exception
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseBean handle401() {
        return new ResponseBean(401, "Unauthorized", null);
    }

    //Catch all other exceptions
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
        return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }
}

Configure Shiro

You can take a look at the officialSpring-ShiroIntegration of the tutorial, have a preliminary understanding. But now that we use itSpring-BootThen we must strive for zero configuration file.

Implementation of jwttoken

JWTTokenAlmostShiroThe carrier of user name and password. Because we are front-end and back-end separation, the server does not need to save the user state, so it does not need toRememberMeThis kind of function, we simply implementAuthenticationTokenInterface. becausetokenI have already included the user name and other information, so here I make a field. If you like to study, you can see the officialUsernamePasswordTokenHow is it achieved.

public class JWTToken implements AuthenticationToken {

    //Key
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

Realize realm

realmWe need to implement it ourselves to deal with the legality of users.

@Service
public class MyRealm extends AuthorizingRealm {

    private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    /**
     *Big hole! , this method must be overridden, otherwise Shiro will report an error
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     *  This method is called only when you need to detect user permissions, such as checkrole , Checkpermission or something
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        UserBean user = userService.getUser(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole(user.getRole());
        Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
        simpleAuthorizationInfo.addStringPermissions(permission);
        return simpleAuthorizationInfo;
    }

    /**
     *By default, this method is used to verify whether the user name is correct or not, and an exception is thrown.
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        //Decrypt to get the user name, which is used to compare with the database
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        UserBean userBean = userService.getUser(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");
    }
}

staydoGetAuthenticationInfo()The user can customize to throw many exceptions. See the document for details.

Rewrite filter

All requests go through firstFilterSo we inherit the officialBasicHttpAuthenticationFilterAnd rewrite the authentication method.

Code execution processpreHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin

public class JWTFilter extends BasicHttpAuthenticationFilter {

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

    /**
     *Determine whether the user wants to log in.
     *Check whether the header contains the 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 there is an error, it will throw an exception and be caught
        getSubject(request, response).login(token);
        //If no exception is thrown, the login is successful and true is returned
        return true;
    }

    /**
     *  Here we will explain in detail why the final return is true, that is, access is allowed
     *  For example, we provide an address / article
     *Login users and visitors see different content
     *If false is returned here, the request will be intercepted directly and the user will not see anything
     *So we return true here. In the controller, we can use the subject.isAuthenticated () to determine whether the user is logged in
     *  If some resources can only be accessed by login users, we just need to add the @ Require authentication annotation
     *  But there is a drawback to doing so, that is, it can't be used to get , Post and other requests are filtered and authenticated separately ( Because we rewrote the official method ) But it has little effect 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;
    }

    /**
     *Support for cross domain
     */
    @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"));
        //When cross domain, an option request will be sent first. Here, we will directly return the normal state to the option request
        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());
        }
    }
}

getSubject(request, response).login(token);This step is to submit to therealmTo deal with.

Configure Shiro

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //  Use your own realm
        manager.setRealm(realm);

        /*
         *  Close the session provided by Shiro. See the document for details
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        //Add your own filter and name it JWT
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");

        /*
         *Custom URL rules
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        //All requests go through our own JWT filter
        filterRuleMap.put("/**", "jwt");
        //Visiting 401 and 404 pages does not pass our filter
        filterRuleMap.put("/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     *The following code is to add annotation support
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        //Force the use of cglib to prevent duplicate agents and problems that may cause agent errors
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

You can refer to the document for the URL ruleshttp://shiro.apache.org/web.html

summary

I’ll just talk about the improvement of the code

  • Shiro’sCacheFunction.
  • When the authentication fails in Shiro, the 401 information can not be returned directly. Instead, it can be transferred to the/401Address implementation.

GitHub address and source:https://github.com/Smith-Crui…

Recommended Today

A package may not list itself as a dependency

A package may not list itself as a dependency Today, when using flutter to develop app, runningflutter package getCommand, the console reported a package may not list itself as a dependency error Problem presentation functionflutter package getThe console reported the following error: Error on line 27, column 3 of pubspec.yaml: A package may not list […]