GitHub project address: github.com/Smith-Cruis… .

The original address: www.inlighting.org/archives/sp… .

The preface

I am also a half-way person, if you have any good comments or criticism, please be sure to issue below.

If you want to experience it directly, clone the project directly and run the MVN spring-boot:run command to access it. See the url rules at the end of the tutorial.

If you want to learn more about Spring Security, check it out

Spring Boot 2.0+Srping Security+Thymeleaf

Spring Boot 2 + Spring Security 5 + JWT Single page Application Restful Solution (recommended)

features

  • Fully use Shiro’s annotation configuration to maintain a high degree of flexibility.
  • Discard cookies and sessions and use JWT to implement stateless authentication.
  • JWT keys support expiration time.
  • Cross-domain support is provided.

The preparatory work

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

  • Spring Boot basic syntax, at least to understandControllerRestControllerAutowiredAnd so on. Just take a look at the official getting-Start tutorial.
  • JWT (Json Web Token) basic concepts, and simple manipulation of JWT JAVA SDK.
  • For basic Shiro operations, see the official 10 Minute Tutorial.
  • To simulate HTTP request tools, I used PostMan.

A quick explanation of why we want to use JWT, because we want to achieve full back end separation, so it’s not possible to use sessions, cookies for authentication, so JWT is used, you can use an encryption key for the back end authentication.

The program logic

  1. We POST the username and password to/loginTo log in, return an encrypted token on success, or error 401 on failure.
  2. After that, each request for access to a url that requires permission must be made inheaderaddAuthorizationFields, for exampleAuthorization: tokentokenFor the key.
  3. The background is going to betokenIf there is a mistake, return 401 directly.

Token Encryption

  • Carry theusernameThe information is in the token.
  • The expiration time is set.
  • Use the user login password pairtokenTo encrypt.

Token Verification Process

  1. To obtaintokenCarried in theusernameInformation.
  2. Go into the database and search 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 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 the 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 package 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>
            <! -- specify the JDK compile 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>
Copy the code

Be careful to specify the JDK version and encoding.

Build a simple data source

To reduce the code for the tutorial, I used HashMap to locally simulate a database with the following structure:

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

This is one of the simplest user rights table, if you want to further understand, baidu RBAC.

A UserService is then built to simulate the database query and the results are put into a UserBean.

UserService.java

@Component
public class UserService {

    public UserBean getUser(String username) {
        // Return null without this user
        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"));
        returnuser; }}Copy the code

UserBean.java

public class UserBean {
    private String username;

    private String password;

    private String role;

    private String permission;

    public String getUsername(a) {
        return username;
    }

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

    public String getPassword(a) {
        return password;
    }

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

    public String getRole(a) {
        return role;
    }

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

    public String getPermission(a) {
        return permission;
    }

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

Configuration JWT

We write a simple JWT encryption, verification tool, and use the user’s own password as the encryption key, so that the token can be intercepted by others can not be cracked. And we included username information in the token, and set the key to expire in 5 minutes.

public class JWTUtil {

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

    /** * Verify the token is correct *@paramToken key *@paramSecret Specifies the user password *@returnCorrect */
    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; }}/** * Obtain the information in the token without secret decrypting *@returnUser name */ contained in the token
    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 after 5min *@paramUsername indicates the username *@paramSecret Specifies the user password *@returnEncrypted token */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // Attach the username information
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null; }}}Copy the code

Build the URL

ResponseBean.java

Since we want to be restful, we want to make sure that the format of the return is the same every time, so I set up a ResponseBean to be consistent with the format of the return.

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

    // Return information
    private String msg;

    // The 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(a) {
        return code;
    }

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

    public String getMsg(a) {
        return msg;
    }

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

    public Object getData(a) {
        return data;
    }

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

Custom exception

In order to realize my own can be an exception is thrown by hand, I wrote a UnauthorizedException. Java

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

    public UnauthorizedException(a) {
        super();
    }
}
Copy the code

URL structure

URL role
/login Log in to
/article It’s accessible to everyone, but users and visitors don’t see the same content
/require_auth Only the login user can access it
/require_role Only admin users can log in
/require_permission Access is available only to users with view and Edit permissions

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 newUnauthorizedException(); }}@GetMapping("/article")
    public ResponseBean article(a) {
        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(a) {
        return new ResponseBean(200."You are authenticated".null);
    }

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

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

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

Handling framework exceptions

As mentioned earlier, restful is about unifying the return format, so we also need to handle Spring Boot’s throw globally. This is done well with @RestControllerAdvice.

@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 UnauthorizedException
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseBean handle401(a) {
        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;
        }
        returnHttpStatus.valueOf(statusCode); }}Copy the code

Configure Shiro

You can first look at the official Spring-Shiro integration tutorial, have a preliminary understanding. But since we’re using Spring-boot, we’re definitely going for zero configuration files.

Implement JWTToken

JWTToken is basically a carrier for Shiro’s username and password. Since we are separated from the front and back ends, the server does not need to save the user state, so there is no need for functions like RememberMe. We simply implement the AuthenticationToken interface. Since the token itself already contains the user name and other information, I have made a field here. If you like to delve deeper, take a look at how the official UsernamePasswordToken is implemented.

public class JWTToken implements AuthenticationToken {

    / / key
    private String token;

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

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

    @Override
    public Object getCredentials(a) {
        returntoken; }}Copy the code

Realize the Realm

The part of the realm that handles whether a user is legitimate needs to be implemented.

@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;
    }

    /** * You must override this method otherwise Shiro will report an error */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    / * * * only when the user permission to check this method is called, for example checkRole, checkPermission * /
    @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;
    }

    /** * This method is used by default to verify that the user name is correct. * /
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // Decrypt to obtain username for comparison with 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"); }}Copy the code

In doGetAuthenticationInfo(), you can throw a lot of exceptions. See the documentation for details.

Rewrite the Filter

All requests through the Filter, so we inherit official BasicHttpAuthenticationFilter, and rewrite the authentication method.

Code execution process preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin.

public class JWTFilter extends BasicHttpAuthenticationFilter {

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

    /** * Determine if 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");
        returnauthorization ! =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 it to realm for login. If an error occurs, it will throw an exception and be caught
        getSubject(request, response).login(token);
        // Return true if no exception is thrown
        return true;
    }

    If false is returned, the request will be blocked. If false is returned, the request will be blocked. If false is returned, the request will be blocked. The user can't see anything * so we return true here, and the Controller can tell if the user is logged in by subject.isauthenticated () * If there are resources that only the logged user can access, we just add it to the method@RequiresAuthenticationThe drawback of this approach is that it doesn't filter out GET,POST, etc. (because we overwrote the official method), but it doesn't really affect 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;
    }

    /** * Cross-domain support */
    @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 we cross domains, we first send an option request. In this case, we 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);
    }

    /** * Redirect the invalid 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()); }}}Copy the code

getSubject(request, response).login(token); This is where you submit to the realm for processing.

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 shiro's built-in session, See the document * 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");

        / * * * http://shiro.apache.org/web.html#urls- * / custom url rules
        Map<String, String> filterRuleMap = new HashMap<>();
        // All requests go through our own JWT Filter
        filterRuleMap.put("/ * *"."jwt");
        // Access to 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(a) {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // Force the use of cGlib to prevent duplicate agents and problems that can cause agent errors
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        returnadvisor; }}Copy the code

Inside the URL rule their shiro.apache.org/web.html reference documentation.

conclusion

I’m just going to talk about where the code could be improved

  • No implementation of ShiroCacheFunction.
  • Shiro does not directly return 401 information when authentication fails. Instead, Shiro jumps to/ 401Address implementation.