Preface I am also a halfway person, if you have any good opinion or criticism, please be sure to issue. Project address: github.com/Smith-Cruis… . Clone the project directly by running the MVN spring-boot:run command. Url rules to see after the tutorial. Spring Boot 2 + Spring Security 5 + JWT Spring Boot 2.0+Srping Security+Thymeleaf Single page application Restful solution feature

Fully uses Shiro’s annotation configuration to maintain a high degree of flexibility. Cookie and Session are abandoned, and JWT is used for authentication, fully realizing stateless authentication. JWT keys support expiration time. Cross-domain support is provided

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

Basic Spring Boot syntax, at least basic comments like Controller, RestController, Autowired, etc. Look no further than the official getting-start tutorial. Basic concepts of JWT (Json Web Token), and will work with JWT’s JAVA SDK. For the basics of Shiro, see the official 10 Minute Tutorial. To simulate HTTP request tools, I used PostMan.

Just to give you a brief explanation of why we use JWT, because we want to achieve complete front and back end separation, so it’s not possible to use session, cookie authentication, so JWT comes in handy, you can use an encryption key to do front and back end authentication. The program logic

We POST the username and password to /login to login, return an encryption token if successful, return 401 error if failed. The user must add the Authorization field in the header for each ACCESS request, for example, Authorization: Token. Token is a key. The background will verify the token and return 401 if there is any misunderstanding.

Token Encryption

Username information is carried in the token. Set expiration time. Encrypt the token using the user login password.

Token Verification Process

Get the username information carried in the token. Go into the database and search for the user and get his password. Use the user’s password to verify that the token is correct.

Prepare the Maven file to create a Maven project and add related dependencies.

4.0.0

< the groupId > org. Inlighting < / groupId > < artifactId > shiro - study < / artifactId > < version > 1.0 - the SNAPSHOT < / version > < dependencies > < the 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> <! <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> <! Plugins </groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build>Copy the code

username password role permission

smith smith123 user view

danny danny123 admin view,edit

This is a simple user permission table, if you want to learn more, baidu RBAC. Then build a UserService to simulate a database query and put the results into a UserBean. UserService.java @Component public class UserService {

Public UserBean getUser(String username) {// Return null 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; }Copy the code

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;
}
Copy the code

} We write a simple JWT encryption and verification tool, and use the user’s own password as the encryption key, so as to ensure that the token can not be cracked even if intercepted by others. In addition, we attach username information to the token, and set the key to expire in 5 minutes. public class JWTUtil {

Private static final Long EXPIRE_TIME = 5*60*1000; private static final Long EXPIRE_TIME = 5*60*1000; @param secret User password @param secret User password @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(); DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception exception) { return false; }} public static String getUsername(String token) {try {public static String getUsername(String token) { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } catch (JWTDecodeException e) { return null; * @param username username * @param secret user password * @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); Return jwt.create ().withclaim ("username", username).withexpiresat (date).sign(algorithm); } catch (UnsupportedEncodingException e) { return null; }}Copy the code

If we want to be restful, we want to make sure that the ResponseBean returns the same format each time, so I created a ResponseBean to standardize the format. public class ResponseBean {

Private int code; // Return message private String MSG; // Return data 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; }Copy the code

To enable myself to manually throw exceptions, I wrote a UnauthorizedException. Java public class UnauthorizedException extends RuntimeException {public UnauthorizedException(String msg) { super(msg); }

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

} Copy the code URL structure

URL function

Login/login

Everyone can access it, but users and visitors see different things

/require_auth Only logged-in users can access

/require_role Only the role user of admin can log in

/require_permission Only accessible by 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 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);
}
Copy the code

} Copy code to handle framework exceptions. As mentioned earlier, restful returns a uniform format, so we also need to handle Spring Boot exceptions globally. This works well with @RestControllerAdvice. @RestControllerAdvice public class ExceptionController {

@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() { return new ResponseBean(401, "Unauthorized", null); @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); }Copy the code

}

Copy the code to configure Shiro. You can take a look at the official Spring-Shiro integration tutorial to get a start. But since we’re using Spring-Boot, we’ll definitely strive for zero configuration files. Implementing JWTToken JWTToken is basically a carrier for Shiro username and password. Because we are front and back separated, the server does not need to keep user status, so there is no need for functions like RememberMe. We simply implement the AuthenticationToken interface. Since the token itself already contains information such as the user name, I have created 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() { return token; } @Override public Object getCredentials() { return token; }Copy the code

} copy the code to implement Realm Realm to handle whether the user is legitimate, we need to implement our own. @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; } /** * pit! */ @override public Boolean supports(AuthenticationToken token) {return token instanceof JWTToken; } /** * this method is called only when user permissions need to be checked, CheckRole, for example, such as 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; } /** * By default, this method is used to verify whether the user name is correct. */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { String token = (String) auth.getCredentials(); String username = jwtutil.getUsername (token); 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

} Duplicate code in doGetAuthenticationInfo allows users to throw a number of custom exceptions, as described in the documentation. Rewrite the Filter all requests through the Filter, so we inherit official BasicHttpAuthenticationFilter, and rewrite the authentication method. PreHandle ->isAccessAllowed->isLoginAttempt->executeLogin public class JWTFilter extends BasicHttpAuthenticationFilter {

private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); /** * Determine whether the user wants to log in. 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); GetSubject (Request, response).login(token); // Submit to realm for login. // If no exception is thrown, the login is successful. Return true; } /** * if false is returned, the request will be blocked. The user can't see anything * so we return true. The Controller can use subject.isauthenticated () to determine if the user is logged in. We just need to add @requiresAuthentication to the method * but this has the disadvantage of not being able to filter the GET,POST and other requests separately (because we rewrote the official method). 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")); // An option request is first sent across domains, Here we give the option to request directly to return to normal state if (it. GetMethod () equals (RequestMethod. OPTIONS. The name ())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); 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

} copy the code getSubject(request, response).login(token); Shiro@configuration Public class ShiroConfig {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"); / custom url rules * * * http://shiro.apache.org/web.html#urls- * / Map < String, the String > filterRuleMap = new HashMap < > (); // All requests pass through our own JWT Filter filterrulemap. put("/**", "JWT "); // Access 401 and 404 pages without passing our Filter filterrulemap. put("/401", "anon"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } / * * * the following code is to add annotations support * / @ Bean @ DependsOn (" lifecycleBeanPostProcessor ") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); // Enforce cglib, Prevent repeated agents and agents may cause error problems / / 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; }Copy the code

} copying code inside the URL rule to reference documentation at http://shiro.apache.org/web.html. So let me conclude by saying what we can do to improve the code

Shiro’s Cache functionality is not implemented. Shiro does not directly return 401 information when authentication fails. Instead, Shiro does this by jumping to /401 address.

Author: Smith Link: juejin.cn/post/684490… The copyright belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.