Springboot+Shiro+Redis前后端分离单点登录式权限管理系统

Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。

具体详细介绍百度搜索,这里就不再过多描述了。

pom.xml文件

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>auth-service</artifactId>
    <version>2.0</version>
    <name>auth-service</name>
    <description>Shiro auth project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.49</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
            <scope>compile</scope>
        </dependency>
        <!--JPA-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--JDBC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--mysql-connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.1</version>
        </dependency>
        <!-- shiro+redis缓存插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

项目结构图:

实现代码

ShiroConfig配置自定义拦截器AuthFilter,需要过滤的接口;安全管理器SecurityManager;自定义AuthorizingRealm安全登陆和权限认证;自定义加密验证规则和Redis会话缓存。

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("拦截器 =>>>> ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //自定义AuthFilter拦截器 auth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("auth", new AuthFilter());
        shiroFilterFactoryBean.setFilters(filters);
        //拦截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        //登陆过滤接口
        filterChainDefinitionMap.put("/toLogin", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/auth", "anon");
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/**", "auth");

        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 安全管理器
     * @return
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义realm
        securityManager.setRealm(myShiroRealm());

        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(redisCacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());

        return securityManager;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        //散列算法:这里使用MD5算法;   加密两次
        CustomCredentialsMatcher matcher = new CustomCredentialsMatcher(SysUserUtils.hashAlgorithmName);
        matcher.setHashIterations(SysUserUtils.hashIterations);
        matcher.setStoredCredentialsHexEncoded(true);
//        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//        hashedCredentialsMatcher.setHashAlgorithmName("md5");
//        hashedCredentialsMatcher.setHashIterations(2);
        return matcher;
    }

    @Bean
    public AuthRealm myShiroRealm() {
        AuthRealm myShiroRealm = new AuthRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    @Value("${spring.redis.database}")
    private Integer database;

    @Value("${spring.redis.password}")
    private String password;

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     * @return
     */
    public RedisManager getRedisManager() {
        RedisManager redisManager = new RedisManager();
        // 默认 127.0.0.1:6379
        redisManager.setDatabase(database);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     * @return
     */
    public RedisCacheManager redisCacheManager() {
        logger.info("创建RedisCacheManager...");
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(getRedisManager());
        redisCacheManager.setPrincipalIdFieldName("userId");//不添加此编注会异常
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(getRedisManager());
        return redisSessionDAO;
    }

    /**
     * Session Manager 会话管理器
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }
}

安全登陆验证、token验证

import com.example.config.token.AuthToken;
import com.example.config.token.SysUserPrincipal;
import com.example.sys.service.SysTokenService;
import com.example.sys.service.SysUserService;
import com.example.sys.entity.SysMenu;
import com.example.sys.entity.SysRole;
import com.example.sys.entity.SysToken;
import com.example.sys.entity.SysUser;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author: tcq
 * @date: 2021-11-01 13:19
 */
@Component
public class AuthRealm extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysTokenService sysTokenService;

    /**
     * 授权 获取用户的角色和权限
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1. 从 PrincipalCollection 中来获取登录用户的信息
        SysUserPrincipal userPrincipal = (SysUserPrincipal) principals.getPrimaryPrincipal();
        SysUser sysUser = sysUserService.findByUserId(userPrincipal.getUserId());

        //2.添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (SysRole role : sysUser.getRoleList()) {
            //2.1添加角色
            System.out.println(role.getRole());
            simpleAuthorizationInfo.addRole(role.getRole());
            for (SysMenu permission : role.getPermissions()) {
                //2.1.1添加权限
                simpleAuthorizationInfo.addStringPermission(permission.getMenu());
                System.out.println(permission.getMenu());
            }
        }
        return simpleAuthorizationInfo;
    }

    /**
     * 登录时-shiro自动认证,接口访问验证token
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("AuthRealm.doGetAuthenticationInfo");
        //获取token
        AuthToken authToken = (AuthToken) authenticationToken;
        String token = authToken.getToken();
        if (StringUtils.isBlank(token)){ // 登录验证
            SysUser sysUser = sysUserService.findByUsername(authToken.getUsername());
            if (sysUser == null) {
                throw new UnknownAccountException("用户不存在!");
            }
            SysUserPrincipal userPrincipal = new SysUserPrincipal(sysUser.getUserId(), sysUser.getUsername(), sysUser.getName());
            //交给AuthenticationInfo去验证密码是否正确,成功后记录SysUserPrincipal
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                    userPrincipal,
                    sysUser.getPassword(),
                    ByteSource.Util.bytes(sysUser.getCredentialsSalt()),
                    getName());
            return info;
        }else{ //token 认证
            //1. 根据accessToken,查询用户信息
            SysToken tokenEntity = sysTokenService.findByToken(token);
            //2. token失效
            if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
                throw new IncorrectCredentialsException("token失效,请重新登录");
            }
            //3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
            SysUser sysUser = sysUserService.findByUserId(tokenEntity.getUserId());
            //4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
            if (sysUser == null) {
                throw new UnknownAccountException("用户不存在!");
            }
            SysUserPrincipal userPrincipal = new SysUserPrincipal(sysUser.getUserId(), sysUser.getUsername(), sysUser.getName());
            //5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userPrincipal, token, this.getName());
            return info;
        }
    }
}
 
 

自定义凭据匹配器

package com.example.config.shiro;

import com.example.config.token.AuthToken;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;

/**
 * 自定义凭据匹配器
 * @author: tcq
 * @date: 2021-11-02 10:09
 */
public class CustomCredentialsMatcher extends HashedCredentialsMatcher {

    public CustomCredentialsMatcher(String hashAlgorithmName) {
        super(hashAlgorithmName);
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        AuthToken authToken = (AuthToken) token;
        if (StringUtils.isNotBlank(authToken.getToken())){
            return true;
        }
        //交给shiro验证密码是否正确
        return super.doCredentialsMatch(token, info);
    }
}

自定义拦截器AuthFilter

package com.example.config.shiro;

import com.example.config.token.AuthToken;
import com.example.sys.utils.TokenUtil;
import com.example.sys.utils.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * shiro 自定义拦截器
 * @author: tcq
 * @date: 2021-11-01 11:47
 */
@Component
public class AuthFilter extends AuthenticatingFilter {
    // 定义jackson对象
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);

    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        //String referer = httpServletRequest.getHeader("Referer");
        //System.out.println("referer = " + referer);

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        logger.info("doFilterInternal请求地址:"+ (httpServletRequest).getRequestURI());

        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("text/plain;charset=utf-8");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Content-type, accept, token");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        /*if (StringUtils.isNotBlank(referer)){
            if (referer.endsWith("/")){
                referer = referer.substring(0, referer.length() - 1);
            }
            //httpServletResponse.setHeader("Access-Control-Allow-Origin", referer);
        }*/
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 3、创建Token对象
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("AuthFilter.createToken");
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        return new AuthToken(token);
    }

    /**
     * 步骤1.所有请求全部拒绝访问
     * @param servletRequest
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse response, Object mappedValue) {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (request.getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }
        return false;
    }

    /**
     * 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取请求中的token,再调用executeLogin方法
     * executeLogin方法执行 this.createToken  再执行  getSubject(request, response).subject.login(token);
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("AuthFilter.onAccessDenied");
        //获取请求token,如果token不存在,直接返回
        String token = TokenUtil.getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            responseCode(response, "请先登录");
            //httpResponse.sendRedirect(((HttpServletRequest) request).getContextPath() + "/toLogin");
            return false;
        }
        return executeLogin(request, response);
    }

    /**
     * token失效时候调用
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        System.out.println("AuthFilter.onLoginFailure");
        //处理登录失败的异常
        responseCode(response, "登录凭证已失效,请重新登录");
        return false;
    }


    /**
     * 缺少token或token失效时返回
     * @param servletResponse
     * @param msg
     * @return
     */
    private boolean responseCode(ServletResponse servletResponse, String msg) {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        try {
            String json = MAPPER.writeValueAsString(R.error(401, msg));
            response.getWriter().print(json);
            response.getWriter().flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

 验证流程说明

登陆认证

以上4个核心配置文件,在登陆接口执行过程中,subject.login(authToken);使用了内部验证,调用doGetAuthenticationInfo()方法,因为是登陆请求,不带token,通过username获取用户信息,把用户信息交给shiro内部认证,验证密码时在CustomCredentialsMatcher中调用内部验证方法super.doCredentialsMatch(token, info);

网上有很多案例,是通过密码明文加密比对数据库密文是否相同,来验证是否登陆成功,此处采用了shiro原生的验证方式。

接口token认证

对于非过滤的接口,需要token认证访问。

在自定义拦截器AuthFilter中

步骤一:isAccessAllowed()拦截请求

步骤二:onAccessDenied()检查token参数,必须携带才能继续访问,内部执行源码

protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                    "must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        }
        try {
            Subject subject = getSubject(request, response);
            subject.login(token);
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }
    }

步骤三:创建AuthenticationToken对象去验证

步骤四:AuthRealm.doGetAuthenticationInfo()验证token有效性,验证绑定的用户有效性

步骤五:上一步如验证成功,CustomCredentialsMatcher.doCredentialsMatch()返回true通过认证;如验证失败onLoginFailure()返回json失败消息。

权限认证

注解标注

@RequiresPermissions("user:list")//权限标识

管理员or测试员访问

@RequiresRoles(value = {"admin", "test"}, logical = Logical.OR)

认证方法,AuthRealm.doGetAuthorizationInfo(),验证后会缓存起来;验证失败异常监听返回json消息

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler({UnauthorizedException.class, AuthorizationException.class, Exception.class})
    @ResponseBody
    public R authorizationException(Exception ex) {
        R r = R.error();
        if (ex instanceof AuthorizationException || ex instanceof UnauthorizedException) {
            r = R.error(403, "没有操作权限");
        }
        return r;
    }
}

登陆接口

http://localhost:6080/shiro/auth

登陆成功后,生成token并返回

@RequestMapping("/auth")
    @ResponseBody
    public R testLogin(@RequestParam String username, @RequestParam String password) {
        Subject subject = SecurityUtils.getSubject();
        //测试
        AuthToken authToken = new AuthToken(username, password);
        try {
            subject.login(authToken);
        }catch (UnknownAccountException e){
            return R.error("账号不存在");
        }catch (IncorrectCredentialsException e){
            return R.error("密码不正确");
        }catch (Exception e){
            e.printStackTrace();
            return R.error(e.getMessage());
        }
        SysUserPrincipal user = getUser();

        return R.ok(user).put("token", sysTokenService.createToken(user.getUserId()));
    }

token生成规则

public String createToken(Long userId) {
        //生成一个token
        String token = RandomStringUtils.randomAlphanumeric(16);
        //当前时间
        Date now = new Date();
        //过期时间
        Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
        //判断是否生成过token
        SysToken tokenEntity = tokenRepository.findByUserId(userId);
        if (tokenEntity == null) {
            tokenEntity = new SysToken();
            tokenEntity.setUserId(userId);
            tokenEntity.setToken(token);
            tokenEntity.setUpdateTime(now);
            tokenEntity.setExpireTime(expireTime);
            //保存token
            tokenRepository.save(tokenEntity);
        } else {
            //token 没过期 继续使用
            if (tokenEntity.getExpireTime().getTime() > System.currentTimeMillis()) {
                token = tokenEntity.getToken();
            }
            tokenEntity.setToken(token);
            tokenEntity.setUpdateTime(now);
            tokenEntity.setExpireTime(expireTime);
            //更新token
            tokenRepository.save(tokenEntity);
        }
        //redisUtils.set(authKey + token, tokenEntity, EXPIRE);
        return token;
    }

项目源码:https://download.csdn.net/download/qq_36100599/38488131

Guess you like

Origin blog.csdn.net/qq_36100599/article/details/121509780