features

  • 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.

The preparatory work

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

  • Basic Spring Boot syntax, at leastControllerRestControllerAutowiredWait for these basic notes. 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

  1. We POST the username and password to/loginLog in and return an encrypted token on success or 401 error on failure.
  2. Subsequent user access to each url requiring permission must be inheaderaddAuthorizationFields, for exampleAuthorization: tokentokenFor the key.
  3. In the backgroundtokenIf there is any misunderstanding, return 401 directly.

Token Encryption

  • Carry theusernameThe information is in the token.
  • Set expiration time.
  • Use the user login password pairtokenEncrypt.

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 the related dependencies.

<? The 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 > 1.0 the SNAPSHOT < version > < / version > < dependencies > < the dependency > < groupId > org.. Apache shiro < / groupId > <artifactId> shro-spring </artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.auth0</groupId> < artifactId > Java - JWT < / artifactId > < version > 3.2.0 < / version > < / dependency > < the 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> </project>Copy the code

Be careful to specify the JDK version and encoding.

Build a simple data source

To simplify the tutorial code, I used HashMap to simulate a database locally, structured as follows:

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) {// 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

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

Configuration JWT

We write a simple JWT encryption and verification tool, and use the user’s own password as the encryption key, so 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; @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

Build the URL

ResponseBean.java

Since we want to be restful, we want to make sure that the returned format is the same every time, so I created a ResponseBean to unify the returned 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

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() { super(); }}Copy the code

URL structure

URL role
/login Log in to
/article Everyone can access it, but users and visitors see different things
/require_auth Only logged-in users can access
/require_role Only the admin user can log in
/require_permission Only users with View and Edit permissions 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); }}Copy the code

Handling frame exceptions

As mentioned earlier, restful returns should be consistent, 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

Configure Shiro

Take a look at the official Spring-Shiro integration tutorial to get an idea. But since we’re using Spring-Boot, we’ll definitely strive for zero configuration files.

Implement JWTToken

JWTToken is basically a carrier for Shiro’s 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 {// 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

Realize the Realm

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

@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

In doGetAuthenticationInfo() you can 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.

Executables 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

getSubject(request, response).login(token); This step is submitted to 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"); / 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

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

conclusion

Let me just say what we can do to improve the code

  • Shiro is not implementedCacheFunction.
  • Shiro cannot directly return 401 information when authentication fails, but instead jumps to/ 401Address implementation.

Github address and source: github.com/Smith-Cruis…

This article was first published on the public account: Java Version of the Web project, welcome to pay attention to get more exciting content