shiro-jwt

Preface

Project address: github.com/Smith-Cruis ...

If you want to experience it directly, directly clone the project and run the mvn spring-boot: run command to access it. See the tutorial for the URL rules by yourself.

If you want to understand Spring Security, you can see

Simple tutorial for Spring Boot 2.0 + Srping Security + Thymeleaf

Spring Boot 2 + Spring Security 5 + JWT single page application Restful solution

Features
Fully use Shiro's annotation configuration to maintain a high degree of flexibility.
Give up Cookies and Session, use JWT for authentication, and fully realize stateless authentication.
The JWT key supports expiration time.
Support
for cross-domain preparations
Before starting this tutorial, please ensure that you are familiar with the following points.

Spring Boot basic syntax, at least to understand the basic notes such as Controller, RestController, Autowired. In fact, just look at the official Getting-Start tutorial.
The basic concept of JWT (Json Web Token), and will simply operate JWT's JAVA SDK.
Shiro's basic operations, just look at the official 10 Minute Tutorial.
To simulate HTTP request tool, I use PostMan.
Briefly explain why we should use JWT, because we want to achieve complete front-end separation, so it is impossible to use session, cookie authentication, so JWT is used, you can use an encryption key to Perform front-end and back-end authentication.

Program logic
Our POST user name and password go to / login to log in. If it successfully returns an encrypted token, if it fails, it directly returns a 401 error.
After that, the user must add the Authorization field in the header to access every URL request that requires permissions, such as Authorization: token, and token as the key.
The token will be checked in the background, and if it is wrong, it will directly return to 401.
Token encryption description
carries username information in the token.
The expiration time is set.
Use the user login password to encrypt the token.
Token verification process
Obtain the username information carried in the token.
Enter this database to search for this user and get his password.
Use the user's password to verify that the token is correct.
Prepare Maven file
Create a new Maven project and add related dependencies.

<?xml version="1.0" encoding="UTF-8"?>


4.0.0

<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 打包工具 -->
        <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>
        <!-- 指定JDK编译版本 -->
        <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>
Copy the code, pay attention to specify the JDK version and encoding

Building a simple data source
In order to reduce the code of the tutorial, I used HashMap to simulate a database locally, 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, Baidu RBAC.

Then build a UserService to simulate database query, and put the result in 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 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 code To
configure JWT,
we write a simple JWT encryption and verification tool, and use the user's own password as an encryption key, which ensures that the token cannot be cracked even if it is intercepted by others. And we have attached the username information in the token and set the key to expire in 5 minutes.

public class JWTUtil {

// 过期时间5分钟
private static final long EXPIRE_TIME = 5*60*1000;

/**
 * 校验token是否正确
 * @param token 密钥
 * @param secret 用户的密码
 * @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;
    }
}

/**
 * 获得token中的信息无需secret解密也能获得
 * @return token中包含的用户名
 */
public static String getUsername(String token) {
    try {
        DecodedJWT jwt = JWT.decode(token);
        return jwt.getClaim("username").asString();
    } catch (JWTDecodeException e) {
        return null;
    }
}

/**
 * 生成签名,5min后过期
 * @param username 用户名
 * @param secret 用户的密码
 * @return 加密的token
 */
public static String sign(String username, String secret) {
    try {
        Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        return JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .sign(algorithm);
    } catch (UnsupportedEncodingException e) {
        return null;
    }
}

}
Copy the code
Building the URL
ResponseBean.java
Since we want to achieve restful, then we want to ensure that the format returned every time is the same, so I created a ResponseBean to uniformly return the format.

public class ResponseBean {

// http 状态码
private int code;

// 返回信息
private String msg;

// 返回的数据
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 that I can manually throw an exception, I wrote an UnauthorizedException.java

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

public UnauthorizedException() {
    super();
}

}
Copy the code
URL structure
URL role
/ login login
/ article everyone can access, but the user and visitors see different content
/ require_auth login users can access
/ require_role admin role users can log in
/ require_permission with view and Only users with edit permission can access
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
Handle framework exceptions As
I said before, restful should unify the return format, so we also have to handle exceptions thrown by Spring Boot globally. Using @RestControllerAdvice can be achieved very well.

@RestControllerAdvice
public class ExceptionController {

// 捕捉shiro的异常
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ShiroException.class)
public ResponseBean handle401(ShiroException e) {
    return new ResponseBean(401, e.getMessage(), null);
}

// 捕捉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 To
configure Shiro,
you can first take a look at the official Spring-Shiro integration tutorial to have a preliminary understanding. But since we used Spring-Boot, then we definitely have to fight for zero configuration files.

Implementing JWTToken
JWTToken is almost the carrier of Shiro username and password. Because we are separate front-end and back-end, the server does not need to save the user state, so no such functions as RememberMe, we simply implement the AuthenticationToken interface. Because the token itself already contains information such as the user name, so I made a field here. If you like to delve into it, you can see 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
Realm Realm
is used to handle whether the user is legal or not, we need to implement it 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;
}

/**
 * 大坑!,必须重写此方法,不然Shiro会报错
 */
@Override
public boolean supports(AuthenticationToken token) {
    return token instanceof JWTToken;
}

/**
 * 只有当需要检测用户权限的时候才会调用此方法,例如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;
}

/**
 * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
    String token = (String) auth.getCredentials();
    // 解密获得username,用于和数据库进行对比
    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 code
In doGetAuthenticationInfo, users can custom throw many exceptions, see the documentation for details.


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

Code execution flow preHandle-> isAccessAllowed-> isLoginAttempt-> executeLogin

public class JWTFilter extends BasicHttpAuthenticationFilter {

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

/**
 * 判断用户是否想要登入。
 * 检测header里面是否包含Authorization字段即可
 */
@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);
    // 提交给realm进行登入,如果错误他会抛出异常并被捕获
    getSubject(request, response).login(token);
    // 如果没有抛出异常则代表登入成功,返回true
    return true;
}

/**
 * 这里我们详细说明下为什么最终返回的都是true,即允许访问
 * 例如我们提供一个地址 GET /article
 * 登入用户和游客看到的内容是不同的
 * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
 * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
 * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
 * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
 */
@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"));
    // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
    if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
        httpServletResponse.setStatus(HttpStatus.OK.value());
        return false;
    }
    return super.preHandle(request, response);
}

/**
 * 将非法请求跳转到 /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 step is submitted to realm for processing

配置Shiro
@Configuration
public class ShiroConfig {

@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm) {
    DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
    // 使用自己的realm
    manager.setRealm(realm);

    /*
     * 关闭shiro自带的session,详情见文档
     * 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();

    // 添加自己的过滤器并且取名为jwt
    Map<String, Filter> filterMap = new HashMap<>();
    filterMap.put("jwt", new JWTFilter());
    factoryBean.setFilters(filterMap);

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

    /*
     * 自定义url规则
     * http://shiro.apache.org/web.html#urls-
     */
    Map<String, String> filterRuleMap = new HashMap<>();
    // 所有请求通过我们自己的JWT Filter
    filterRuleMap.put("/**", "jwt");
    // 访问401和404页面不通过我们的Filter
    filterRuleMap.put("/401", "anon");
    factoryBean.setFilterChainDefinitionMap(filterRuleMap);
    return factoryBean;
}

/**
 * 下面的代码是添加注解支持
 */
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    // 强制使用cglib,防止重复代理和可能引起代理出错的问题
    // 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
. You can refer to the document for the URL rule http://shiro.apache.org/web.html.

In conclusion,
I will talk about what can be improved in the next code.

Shiro's Cache function is not implemented.
In Shiro, if the authentication fails, the 401 information cannot be returned directly, but by jumping to the / 401 address.
Reprinted at: https://juejin.im/post/59f1b2766fb9a0450e755993

Published 0 original articles · liked 0 · visits 1

Guess you like

Origin blog.csdn.net/weixin_43403476/article/details/105582132
jwt