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 understand
Controller
、RestController
、Autowired
And 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
- We POST the username and password to
/login
To log in, return an encrypted token on success, or error 401 on failure. - After that, each request for access to a url that requires permission must be made in
header
addAuthorization
Fields, for exampleAuthorization: token
,token
For the key. - The background is going to be
token
If there is a mistake, return 401 directly.
Token Encryption
- Carry the
username
The information is in the token. - The expiration time is set.
- Use the user login password pair
token
To encrypt.
Token Verification Process
- To obtain
token
Carried in theusername
Information. - Go into the database and search for the user and get his password.
- Use the user’s password to verify
token
Is 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 Shiro
Cache
Function. - Shiro does not directly return 401 information when authentication fails. Instead, Shiro jumps to
/ 401
Address implementation.