Learn Spring Boot: (13) Configure Shiro authorization authentication

After learning Apache Shiro earlier, it is now used in the project in combination with Spring Boot for related configuration.

text

add dependencies

Add the dependencies in the pom.xmlfile :shiro-spring

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>

RBAC

RBAC 1 is a role-based access control. Permissions are associated with roles, and users are configured with relevant roles to obtain permission information.

Shiro placement

Create a new Shiroconfiguration class ShiroConfig:

package com.wuwii.common.config;

import com.wuwii.module.sys.autho2.OAuth2Realm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。
 * 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,
 * 所以我们需要定义一系列关于URL的规则和访问权限。
 *
 * @author KronChan
 * @version 1.0
 * @since <pre>2018/2/9 10:35</pre>
 */
@Configuration
public class ShiroConfig {

    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdCookieEnabled(true);
        return sessionManager;
    }

    /**
     * 注册安全管理,必须设置 SecurityManager
     *
     * @param oAuth2Realm    认证
     * @param sessionManager 缓存
     * @return
     */
    @Bean
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 可以添加多个认证,执行顺序是有影响的
        securityManager.setRealm(oAuth2Realm);
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //自定义一个oauth2拦截器,不设置就是使用默认的拦截器
        /*Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", new OAuth2Filter());
        shiroFilter.setFilters(filters);*/
        //拦截器
        //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->
        //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
        Map<String, String> filterMap = new LinkedHashMap<>();
        //配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterMap.put("/sys/logout", "logout");
        // 验证码
        filterMap.put("/sys/captcha.jpg", "anon");
        // 设置系统模块下访问需要权限
        filterMap.put("/sys/login", "anon");
        // 自定义的拦截
        //filterMap.put("/sys/**", "oauth2");
        filterMap.put("/sys/**", "authc");
        // 登陆的 url
        shiroFilter.setLoginUrl("/sys/login");
        // 登陆成功跳转的 url
        shiroFilter.setSuccessUrl("/");
        // 未授权的 url
        // shiroFilter.setUnauthorizedUrl("/login.html");
        //未授权界面;
        shiroFilter.setUnauthorizedUrl("/403");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    /**
     * Shiro生命周期处理器
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启Shiro的注解,
     * (如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,
     * 并在必要时进行安全逻辑验证 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    /**
     * 开启 shiro aop注解支持.
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     * 所以我们需要修改下doGetAuthenticationInfo中的代码;
     * )
     *  <b>需要在身份认证中添加 realm.setCredentialsMatcher(hashedCredentialsMatcher())</b>
     * @return
     */
    /*@Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));

        return hashedCredentialsMatcher;
    }*/
}

Definition of Filter Chain:
1. A URL can be configured with multiple filters, separated by commas.
2. When multiple filters are set, all verifications are passed, and they are considered to pass
. 3. Some filters can specify parameters, such as perms, roles

Shiro's built-in FilterChain:

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

* anon: all urls can be accessed anonymously
* authc: authentication is required to access
* user: configuration remember me or authentication can be accessed

custom interceptor (optional)

If you need to define an oauth2 interceptor according to your own needs, you need to inherit and AuthenticatingFilterimplement several methods.

/**
 * oauth2过滤器
 */
public class OAuth2Filter extends AuthenticatingFilter {

    /**
     * logger
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(OAuth2Filter.class);

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return new OAuth2Token(token);
    }

    /**
     *  shiro权限拦截核心方法 返回true允许访问resource,
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return false;
    }

    /**
     * 当访问拒绝时是否已经处理了;
     * 如果返回true表示需要继续处理;
     * 如果返回false表示该拦截器实例已经处理完成了,将直接返回即可。
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            ((HttpServletResponse) response).setStatus(401);
            response.getWriter().print("没有权限,请联系管理员授权");
            return false;
        }
        return executeLogin(request, response);
    }

    /**
     * 鉴定失败,返回错误信息
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        try {
            ((HttpServletResponse) response).setStatus(401);
            response.getWriter().print("没有权限,请联系管理员授权");
        } catch (IOException e1) {
            LOGGER.error(e1.getMessage(), e1);
        }
        return false;
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        String token = httpRequest.getHeader("token");
        //如果header中不存在token,则从参数中获取token
        if (StringUtils.isBlank(token)) {
            return httpRequest.getParameter("token");
        }
        // 还可以实现从 cookie 获取
        Cookie[] cookies = httpRequest.getCookies();
        if(null==cookies||cookies.length==0){
            return null;
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("token")) {
                token = cookie.getValue();
                continue;
            }
        }
        return token;
    }
}

For the specific implementation, please refer to my previous article "Apache Shiro's Interceptor and Authentication"

Authentication implementation

Shiro's authentication process will eventually be executed by Realm, and then Realm's getAuthenticationInfo(token)method .
This method mainly performs the following operations:
1. Checking the submitted token information for authentication
2. Obtaining user information from a data source (usually a database) according to the token information
3. Matching and verifying the user information.
4. An AuthenticationInfo instance that encapsulates the user information will be returned after the authentication is passed.
5. If authentication fails, an AuthenticationException will be thrown.

What we need to do in our application is to customize a Realm class, inherit the AuthorizingRealmabstract class, overload doGetAuthenticationInfo (), and rewrite the method for obtaining user information.

@Component
public class OAuth2Realm extends AuthorizingRealm {
    @Resource
    private ShiroService shiroService;
    @Resource
    private SysUserService sysUserService;


    /**
     * 此方法调用  hasRole,hasPermission的时候才会进行回调.
     *
     * 权限信息.(授权):
     * 1、如果用户正常退出,缓存自动清空;
     * 2、如果用户非正常退出,缓存自动清空;
     * 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。
     * (需要手动编程进行实现;放在service进行调用)
     * 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,
     * 调用clearCached方法;
     * :Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
     *
     *
     * 当没有使用缓存的时候,不断刷新页面的话,这个代码会不断执行,
     * 当其实没有必要每次都重新设置权限信息,所以我们需要放到缓存中进行管理;
     * 当放到缓存中时,这样的话,doGetAuthorizationInfo就只会执行一次了,
     * 缓存过期之后会再次执行。
     *
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUserEntity user =(SysUserEntity) (principals.getPrimaryPrincipal());;

        // 获取该用户权限列表
        Set<String> permsSet = shiroService.getUserPermissions(user.getId());

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

    /**
     * 认证回调函数,登录时调用
     * 首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException;
     * 如果user找到但锁定了抛出锁定异常LockedAccountException;最后生成AuthenticationInfo信息,
     * 交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        SysUserEntity user = sysUserService.queryByUsername(usernamePasswordToken.getUsername());
        //账号不存在、密码错误
        if (user == null) {
            throw new KCException("账号或密码不正确");
        }

        // 交给 shiro 自己去验证,
        // 明文验证
        return new SimpleAuthenticationInfo(
                user, // 存入凭证的信息,登陆成功后可以使用 SecurityUtils.getSubject().getPrincipal();在任何地方使用它
                user.getPassword(),
                getName());

        // 加密的方式
        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
        /*return new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()), // 加盐,可以注册凭证匹配器 HashedCredentialsMatcher 告诉它怎么加密的
                getName());*/

    }
}

Implement the above two methods to complete authentication and authorization verification.

Login implementation

    @PostMapping("/login")
    @ApiOperation("系统登陆")
    public ResponseEntity<String> login(@RequestBody SysUserLoginForm userForm) {
        String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);
        if (!userForm.getCaptcha().equalsIgnoreCase(kaptcha)) {
            throw new KCException("验证码不正确!");
        }
        UsernamePasswordToken token = new UsernamePasswordToken(userForm.getUsername(), userForm.getPassword());
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.login(token);

         //账号锁定
        if (getUser().getStatus() == SysConstant.SysUserStatus.LOCK) {
            throw new KCException("账号已被锁定,请联系管理员");
        }
        return ResponseEntity.status(HttpStatus.OK).body("登陆成功!");
    }

ASD

    @ApiOperation("用于测试,查询")
    @ApiImplicitParam(name = "string", value = "id", dataType = "string")
    @RequiresPermissions("sys:user:list1")
    @GetMapping()
    public ResponseEntity<List<SysUserEntity>> query(@CustomValid String string) {
        return new ResponseEntity<>(sysUserService.query(new SysUserEntity()), OK);
    }

Simply test an example, sys:user:list1adding one more will 1definitely fail the verification, see what the program will do, it will go to the doGetAuthorizationInfo(PrincipalCollection principals)method in the Realm we defined, and execute the query for all the permissions of the user.
After the verification fails, the final program result is as follows:

Caused by: org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public org.springframework.http.ResponseEntity com.wuwii.module.sys.controller.SysUserController.query(java.lang.String)
    at org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor.assertAuthorized(AuthorizingAnnotationMethodInterceptor.java:90)
    ... 77 common frames omitted
Permission annotation
@RequiresAuthentication  
表示当前Subject已经通过login进行了身份验证;即Subject. isAuthenticated()返回true

@RequiresUser  
表示当前Subject已经身份验证或者通过记住我登录的。

@RequiresGuest  
表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。

@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)  
表示当前Subject需要角色admin和user。

@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)  
表示当前Subject需要权限user:a或user:b。  

References


  1. Role-Based Access Control

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325632569&siteId=291194637