shpringboot2安全验证shiro+redis+md5(自定义单向加密方式)

shiro Apacher开源的java安全框架,提供了鉴权、认证、加密、会话管理等功能,相对于security,shiro简单很多。

MD5配置
shiro三个最重要的组件
Subject 主题,任何与当前应用交互的东西,所有subject均绑定securityManager,subject相当于门面,实际处理仍是securityManager。
SecurityManager 安全的核心,与后端任何的交互均走securityManager,相当于SpringMVC的DispatcherServlet。
Realm 鉴权+认证,shiro从Realm中取数据。

1、登录与登出

 @PostMapping(LoginController.URI)
    public Object login(String account, String password) {
        if (StringUtils.isBlank(account) || StringUtils.isBlank(password)) {
            return "账户或密码不能为空!";
        }
        UsernamePasswordToken userToken = new UsernamePasswordToken(account, password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(userToken);
            Session session = subject.getSession();
            SysUser user = (SysUser) subject.getPrincipal();
            String token = (String) session.getId();
            return token;
        } catch (UnknownAccountException e) {
            log.error("账户不存在:{}", e.getMessage());
            return "账户不存在";
        } catch (IncorrectCredentialsException e) {
            log.error("密码错误:{}", e.getMessage());
            return "密码错误";
        } catch (LockedAccountException e) {
            log.error("账户已锁定:{}", e.getMessage());
            return "账户已锁定";
//        } catch (ExcessiveAttemptsException eae) {
//            return "用户名或密码错误次数过多";
        } catch (AuthenticationException e) {
            log.error("验证失败:{}", e.getMessage());
            return "验证失败";
        }
    }

    @GetMapping(LoginController.URI + "/logout")
    public Object logout() {
        Subject subject = SecurityUtils.getSubject();
        if (subject != null && (subject.getPrincipal() != null)) {
            log.info("logout token:{}", subject.getSession().getId());
            subject.getPrincipal();
            subject.logout();
        }
        return "登出成功!";
    }

2、首先配置ShiroConfig的加密方式、迭代次数等

	  @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

2、在ShiroConfig中,加入到鉴权认证中

    @Bean("customAuthorizingRealm")
    public CustomAuthorizingRealm customAuthorizingRealm() {
        CustomAuthorizingRealm customAuthorizingRealm = new CustomAuthorizingRealm();
        customAuthorizingRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customAuthorizingRealm;
    }

3、在鉴权认证CustomAuthorizingRealm中进行盐值的设置

 @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (authenticationToken.getPrincipal() == null) {
            return null;
        }
        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
        SysUser user = userService.getByAccount(userToken.getUsername(), "1");
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        if (!"1".equals(user.getAae100())) {
            throw new LockedAccountException("账户已锁定!");
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(),ByteSource.Util.bytes(user.getAccount()), this.getName());
    }

4、写个测试类,生成一个密码,账户的密码test,盐值test(一般为用户id或账户),迭代次数1024

  public static void main(String[] args) {
        Object obj = new SimpleHash("MD5", "test", ByteSource.Util.bytes("test"), 1024);
        System.out.println(obj);
    }

5、登录测试,可以跟踪到SimpleCredentialsMatcher的equals方法,查看盐值是否正确。

redis管理session
1、pom添加依赖,使用shiro-redis

	<dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
            <exclusions>
                <exclusion>
                    <artifactId>shiro-core</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
            </exclusions>
        </dependency>

2、shiroconfig中设置一个redisManager(连接redis的ip端口及db等),设置sessionDAO用于redis保存的前缀与过期时间等,将session委托给redis进行管理。CustomWebSessionManager从请求的header中获取token。

  @Bean
    public SecurityManager securityManager(@Qualifier("customAuthorizingRealm") CustomAuthorizingRealm customAuthorizingRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
//        securityManager.setCacheManager(cacheManager());
        securityManager.setRealm(customAuthorizingRealm);
        return securityManager;
    }

    @Bean
    public SessionManager sessionManager() {
        CustomWebSessionManager mySessionManager = new CustomWebSessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }

    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDAO.setExpire(1800);
        redisSessionDAO.setKeyPrefix("kq:session:");
        return redisSessionDAO;
    }

    private RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisHost + ":" + redisPort);
        redisManager.setDatabase(redisDataBase);
        return redisManager;
    }

url过滤
1、获取当前需要鉴权的uri,CustomPermissionsAuthorizationFilter,由于我们使用了菜单方式鉴权,不是每个url均需要鉴权的形式,所以修改了此filter,比如/web/teacher/page/pagerno,我们只鉴权/teacher这个路径在sys_fun中是否存在。

private String[] buildPermissions(ServletRequest request) {
        String[] perms = new String[1];
        HttpServletRequest req = (HttpServletRequest) request;
        String path = req.getServletPath();
        String temp = path.replace(BaseUriHead.WEB + SLASH, "");
        if (temp.indexOf(SLASH) != -1) {
            perms[0] = SLASH + temp.substring(0, temp.indexOf(SLASH));
        } else {
            perms[0] = SLASH + temp;
        }
        return perms;
    }

2、debug会发现走到了CustomAuthorizingRealm

自定义加密
1、 自定义密码校验规则,本代码使用Aes加密

public class CredentialMatcher extends SimpleCredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        String password = new String(userToken.getPassword());
        String dbPassword = (String) info.getCredentials();
        return this.equals(AesUtils.encrypt(password), dbPassword);
    }
}

2、在shiroConfig中配置bean

 @Bean("credentialMatcher")
    public CredentialMatcher credentialMatcher() {
        return new CredentialMatcher();
    }
    @Bean
    public CustomAuthorizingRealm customAuthorizingRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher) {
        CustomAuthorizingRealm myShiroRealm = new CustomAuthorizingRealm();
        myShiroRealm.setCredentialsMatcher(matcher);
        return myShiroRealm;
    }

3、在Realm认证中配置

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (authenticationToken.getPrincipal() == null) {
            return null;
        }
        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
        SysUser user = userService.getByAccount(userToken.getUsername(), "1");
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
    }

前后端分离问题
1、有OPTIONS预请求,来进行确认请求是否可用。需要过滤器返回true
CustomPermissionsAuthorizationFilter的preHandle方法
2、无权限会返回自定义的unauth页面,302无法再次进行请求导致无权限访问的401没办法显示
CustomPermissionsAuthorizationFilter,onAccessDenied中直接返回401,不redirect
3、对于超时,可以判断当前session是否新建如果新建为403,CustomPermissionsAuthorizationFilter的onAccessDenied方法

以下为源码【自定义加密方式】

  1. 首先进行shiro配置
@Configuration
public class ShiroConfig {

    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private String redisPort;
    @Value("${spring.redis.database}")
    private Integer redisDataBase;

    @Bean("hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    @Bean("customAuthorizingRealm")
    public CustomAuthorizingRealm customAuthorizingRealm() {
        CustomAuthorizingRealm customAuthorizingRealm = new CustomAuthorizingRealm();
        customAuthorizingRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customAuthorizingRealm;
    }

    @Bean
    public JavaUuidSessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filterMap = new HashMap<>(4);
        filterMap.put("authc", new CustomPermissionsAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        /*
         * anon:所有url都都可以匿名访问
         * authc:所有url都必须认证通过才可以访问
         */
        filterChainDefinitionMap.put("/web/login", "anon");
        filterChainDefinitionMap.put("/web/unauth", "anon");
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/web/**", "authc");
        filterChainDefinitionMap.put("/web/login/logout", "logout");
        shiroFilterFactoryBean.setLoginUrl("/web/unauth");
        shiroFilterFactoryBean.setUnauthorizedUrl("web/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager(@Qualifier("customAuthorizingRealm") CustomAuthorizingRealm customAuthorizingRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
//        securityManager.setCacheManager(cacheManager());
        securityManager.setRealm(customAuthorizingRealm);
        return securityManager;
    }

    @Bean
    public SessionManager sessionManager() {
        CustomWebSessionManager mySessionManager = new CustomWebSessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }

    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDAO.setExpire(1800);
        redisSessionDAO.setKeyPrefix("kq:session:");
        return redisSessionDAO;
    }

    private RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisHost + ":" + redisPort);
        redisManager.setDatabase(redisDataBase);
        return redisManager;
    }

    @Bean("redisCacheManager")
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setPrincipalIdFieldName("userid");
        return redisCacheManager;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

}

  1. 自定义session管理
public class CustomWebSessionManager extends DefaultWebSessionManager {

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        String token = httpServletRequest.getHeader("token");
        if (StringUtils.isNotBlank(token)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "token");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            return super.getSessionId(request, response);
        }
    }

    /**
     *  //重写这个方法为了减少多次从redis中读取session(自定义redisSessionDao中的doReadSession方法)
     * @param sessionKey
     * @return
     */
    @Override
    protected Session retrieveSession(SessionKey sessionKey) {
        Serializable sessionId = getSessionId(sessionKey);
        ServletRequest request = null;
        if (sessionKey instanceof WebSessionKey) {
            request = ((WebSessionKey) sessionKey).getServletRequest();
        }
        if (request != null && sessionId != null) {
            Session session = (Session) request.getAttribute(sessionId.toString());
            if (session != null) {
                return session;
            }
        }
        Session session = super.retrieveSession(sessionKey);
        if (request != null && sessionId != null) {
            request.setAttribute(sessionId.toString(), session);
        }
        return session;
    }

}
  1. 鉴权+认证
@Slf4j
public class CustomAuthorizingRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUser user = (SysUser) principals.getPrimaryPrincipal();
        for (SysRole role : user.getRoles()) {
            authorizationInfo.addRole(role.getRolename());
            for (SysFun fun : role.getFuns()) {
                if ("3".equals(fun.getFuntype())) {
                    authorizationInfo.addStringPermission(fun.getFunurl());
                }
            }
        }
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (authenticationToken.getPrincipal() == null) {
            return null;
        }
        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
        SysUser user = userService.getByAccount(userToken.getUsername(), "1");
        if (user == null) {
            throw new UnknownAccountException("用户不存在!");
        }
        if (!"1".equals(user.getAae100())) {
            throw new LockedAccountException("账户已锁定!");
        }
        return new SimpleAuthenticationInfo(user, user.getPassword(),ByteSource.Util.bytes(user.getAccount()), this.getName());
    }

}

  1. 获取需要鉴权的uri
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {

    private static final String SLASH = "/";

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return super.preHandle(request, response);
    }

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
        return super.isAccessAllowed(request, response, buildPermissions(request));
    }

    private String[] buildPermissions(ServletRequest request) {
        String[] perms = new String[1];
        HttpServletRequest req = (HttpServletRequest) request;
        String path = req.getServletPath();
        String temp = path.replace(BaseUriHead.WEB + SLASH, "");
        if (temp.indexOf(SLASH) != -1) {
            perms[0] = SLASH + temp.substring(0, temp.indexOf(SLASH));
        } else {
            perms[0] = SLASH + temp;
        }
        return perms;
    }
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        if(request instanceof ShiroHttpServletRequest){
            ShiroHttpServletRequest req = (ShiroHttpServletRequest) request;
            HttpSession session = req.getSession();
           if(session.isNew()){
               WebUtils.toHttp(response).sendError(403);
               return false;
           }
        }
        WebUtils.toHttp(response).sendError(401);
        return false;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值