springboot integrates shiro to complete token authentication, and the points that need attention

  rely:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.4.1</version>
</dependency>

1. Custom token inherits UsernamePasswordToken

public class JwtToken extends UsernamePasswordToken {
   private String token;

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public JwtToken(String username, String password) {
        super(username, password);
    }
}

Note: After inheriting the UsernamePasswordToken class, the account and password use the constructor of the UsernamePasswordToken class

2. Customize Realm

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class CustomerRealm extends AuthorizingRealm {

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    //授权

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        /*
          常见的方法:
          addRole(String role):向授权信息中添加角色。
          addRoles(Collection<String> roles):向授权信息中添加多个角色。
          addStringPermission(String permission):向授权信息中添加权限(字符串形式)。
          addStringPermissions(Collection<String> permissions):向授权信息中添加多个权限(字符串形式)。
          setRoles(Set<String> roles):设置授权信息的角色集合。
          setStringPermissions(Set<String> permissions):设置授权信息的权限集合。
          getRoles():获取授权信息中的角色集合。
          getStringPermissions():获取授权信息中的权限集合。
         */
        // 在这里只是设置权限了一个普通的权限:字符串
        authorizationInfo.addStringPermission("user:add:*");
        return authorizationInfo;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) authenticationToken;
        return new SimpleAuthenticationInfo("任意对象","147258369",this.getName());
    }
}

  • Points to be aware of when customizing Realm :

The first point:

        In the doGetAuthenticationInfo method, his return value:
 

return new SimpleAuthenticationInfo("任意对象","147258369",this.getName());

The SimpleAuthenticationInfo  object is to focus on:

  • The first parameter is the user data saved after successful verification, which is an Object type.
  • The second parameter passed is the parameter to be compared with the password:

 First look at this picture, this is my simulated login code, you can see that I passed the account number and password to JwtToken:


And JwtToken inherits   the UsernamePasswordToken class, continue to click into the source code:

 You can see that in the UsernamePasswordToken class, the method corresponding to the account and password is obtained, and then I continue to call the following method in the login controller to log in:

Subject subject = SecurityUtils.getSubject();
subject.login(token);

After the above code is executed, it will enter the doGetAuthenticationInfo method, and after successful login authentication (query the account and password in the database), if the second parameter of SimpleAuthenticationInfo is passed to me to  new JwtToken("123456789", " 147258369 ") If the second parameter is different, the authentication will fail. Personally, I think  the second parameter type of SimpleAuthenticationInfo is : 

 When logging in, pass in the password of the UsernamePasswordToken class inherited by JwtToken:

The names are the same, so I think it will be compared. If they are inconsistent, an error will be reported: (personal guess)

If the authentication fails during login: it will return: IncorrectCredentialsException exception



The second point (the timing of entering the doGetAuthorizationInfo method):

  • The doGetAuthorizationInfo method is not actively called, but passively called. When there is a RequiresPermissions annotation on the requested interface, the method will be executed to determine whether there is a corresponding permission.

Execution process (request the interface with the interception path): use the shrio login method in the filter, and enter the doGetAuthenticationInfo method for authentication. After the authentication is successful, the interface will be requested. If there is shiro's permission or role annotation on the interface, then it will Enter the doGetAuthorizationInfo method to obtain the corresponding permission or role, and then return to the interface, if you have permission, execute the method of the interface



The third point: When I was testing, I found that after adding the AOP dependency, the filter will also intercept the release path. At present, I have not found a solution to this, so I have no choice but to remove the AOP dependency (I don’t know about you. Will it be like this)

Fourth point:

 

3. Set the configuration class of shrio

import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


@Configuration
public class shiroConfig {

    //创建shirofilter,负责拦截所有请求
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("auth", new MyFilter());
        shiroFilterFactoryBean.setFilters(filters);
        //配置系统受限资源
        //配置系统公共资源
        Map<String,String> map  = new LinkedHashMap<>();
        // 在这里,我放行请求的的是我的登录接口
        map.put("/user/login","anon");
        // 其余接口我都要进行拦截 ,拦截器是 new MyFilter()
        map.put("/**","auth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    //创建shiro安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        defaultWebSecurityManager.setSubjectDAO(subjectDAO);
        //配置realm
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    //创建自定义realm
    @Bean
    public Realm getRealm(){
        CustomerRealm customerRealm = new CustomerRealm();
        //设置缓存管理器
        customerRealm.setCachingEnabled(false);
        customerRealm.setAuthenticationCachingEnabled(true);
        customerRealm.setAuthenticationCacheName("AuthenticationCache");
        customerRealm.setAuthorizationCachingEnabled(true);
        customerRealm.setAuthorizationCacheName("AuthorizationCache");
        return customerRealm;
    }

}
  • Points to note about configuration classes:

        When configuring the interception and release path, you must not use hashmap , but use LinkedHashMap, and then when configuring the path , put the requests that do not need to be intercepted first, and generally put /** at the end:

4. Configure the filter:

package com.example.mp_pring.config.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyFilter extends BasicHttpAuthenticationFilter {
    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            return executeLogin(request, response);
        } catch (Exception e) {
            System.out.println("JwtFilter过滤认证失败!");
            return false;
        }
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        // token 一般都是放在请求头中
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Token");

        // 假设已经拿到token,并校验token了,如果校验不通过就直接 return false;
        // token校验成功,拿到账号跟密码
        JwtToken jwtToken = new JwtToken("123456789","147258369");
        try {
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            Subject subject = SecurityUtils.getSubject();
            subject.login(jwtToken);
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        } catch (AuthenticationException e) {
            response.setCharacterEncoding("utf-8");
            response.getWriter().print("error");
            return false;
        }

    }

    /**
     * 对跨域提供支持
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @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);
    }
}


5. Demo:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;

/**
 * @Description:
 * @Author sk
 * @Date: 2023/4/4 18:40
 */
@Controller
@RequestMapping("/user")
public class UserControllerabc {


    @RequestMapping("/login")
    @ResponseBody
    public String login() throws Exception {
        JwtToken token = new JwtToken("123456789","147258369");
        try {
            Subject subject = SecurityUtils.getSubject();
            subject.login(token);
            System.out.println("认证成功");
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("认证失败");
        }
        return "success";
    }

    @RequestMapping("/test")
    @ResponseBody
    @RequiresPermissions("user:add:*")
    public String test() {
        Subject subject = SecurityUtils.getSubject();
        Object principal = subject.getPrincipal();
        PrincipalCollection principals = subject.getPrincipals();
        return "success:验证成功:user:add:*";
    }

    @RequestMapping("/test1")
    @ResponseBody
    @RequiresPermissions("user:add:a")
    public String test1() {
        return "success:验证成功:user:add:a";
    }
}

 These two interfaces require permissions user:add:* and user:add:a to access, in this case:

The permission set is user:add:* , so logically speaking, both test and test1 interfaces can be accessed, and * represents a wildcard

As you can see, all visits were successful. 

  • Modify permissions

I set the permission here to user:add:a , so I can't access the test interface, try a wave:

 


 It can be seen that if there is no permission, an error will be reported directly

6. Demo file:

        This project has been uploaded to this blog, you can download it yourself if you need it, and you can use it directly

7. Attention! ! ! ! ! :

        After I used shrio, I directly used transaction annotations in the controller layer, causing all interfaces in this controller to report 404 (but this did not appear in my other project). After checking, it is the aop execution of shrio and Transactional Time matters: You can refer to other people's blogs:         Interface 404_@transactional controller_Muzi's Mumu's Blog-CSDN Blog

Guess you like

Origin blog.csdn.net/qq_26112725/article/details/130266197