Java -- springboot 配置 Shiro

1、配置依赖

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

2、编写自定义 token

package com.vim.common.shiro;

import org.apache.shiro.authc.UsernamePasswordToken;

public class UserPasswdToken extends UsernamePasswordToken {

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

3、编写验证主体

package com.vim.common.shiro;

import com.vim.modules.web.model.User;
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 UserRealm extends AuthorizingRealm {

    /**
     * 权限校验
     * @param principal
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //1.取出主体信息,注意此处的 User 必须实现 Serializable 序列化
        User user= (User) principal.getPrimaryPrincipal();

        //2.查询权限信息

        //3.添加权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermission("sys:user:list");
        simpleAuthorizationInfo.addStringPermission("sys:user:edit");
        return simpleAuthorizationInfo;
    }

    /**
     * 用户名和密码校验
     * @param token
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.前端 token 信息
        String username = ((UserPasswdToken) token).getUsername();

        //2.后端 token 信息
        User user = new User("admin", "123456");

        //3.保存的主体对象,此处是User
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        return simpleAuthenticationInfo;
    }
}

4、配置 Shiro

package com.vim.common.config;

import com.vim.common.shiro.UserRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    //添加 servlet filter
    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
        filterRegistration.setEnabled(true);
        filterRegistration.addUrlPatterns("/*");
        return filterRegistration;
    }

    //自定义 realm
    @Bean
    public UserRealm userRealm(){
        UserRealm realm = new UserRealm();
        return realm;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());

        //1.配置登录页面
        shiroFilterFactoryBean.setLoginUrl("/loginPage");
        shiroFilterFactoryBean.setSuccessUrl("/test");

        Map<String,String> map = new HashMap<>();
        //2.登录和登出不做权限校验
        map.put("/login","anon");
        map.put("/logout","anon");

        //3.其他所有请求拦截
        map.put("/**","authc");

        //4.配置无权限页面
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 注解相关
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

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

5、控制器

package com.vim.modules.web.controller;

import com.vim.common.shiro.UserPasswdToken;
import com.vim.modules.web.model.User;
import org.apache.shiro.SecurityUtils;
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 java.util.HashMap;
import java.util.Map;

@Controller
public class LoginController {

    @RequestMapping(value = "/loginPage")
    public String loginPage(){
        return "login";
    }

    @RequestMapping(value = "/login")
    @ResponseBody
    public Map<String, String> login(User user){
        Map<String, String> result = new HashMap<>();

        UserPasswdToken token = new UserPasswdToken(user.getUsername(), user.getPassword());
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            result.put("code", "SUCCESS");
        }catch (Exception e){
            e.printStackTrace();
            result.put("code", "FAIL");
        }
        return result;
    }
}
package com.vim.modules.web.controller;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class TestController {

    @RequestMapping(value = "/test")
    @RequiresPermissions({"sys:user:list"})
    public String test(Model model){
        model.addAttribute("username", "admin");
        return "test";
    }
}

6、异常处理器

package com.vim.common.exception;

import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

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

public class BusinessExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (ex instanceof AuthorizationException) {
            ModelAndView mv = new ModelAndView("/unauthorized");
            return mv;
        }
        return null;
    }
}
package com.vim.common.config;

import com.vim.common.exception.BusinessExceptionResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig {

    @Bean
    public BusinessExceptionResolver myExceptionResolver() {
        return new BusinessExceptionResolver();
    }
}

7、登录页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <style>
        body{
            font-family: Microsoft Yahei;
            font-size: 15px;
        }

        fieldset{    width: 680px;    }

        legend{    margin-left: 8px;    }

        .item{
            height: 56px;
            line-height: 36px;
            margin: 10px;
        }

        .item .item-label{
            float: left;
            width: 80px;
            text-align: right;
        }

        .item-text{
            float: left;
            width: 244px;
            height: 16px;
            padding: 9px 25px 9px 5px;
            margin-left: 10px;
            border: 1px solid #ccc;
            overflow: hidden;
        }

        .item-select{
            float: left;
            height: 34px;
            border: 1px solid #ccc;
            margin-left: 10px;
            font-size: 14px;
            padding: 6px 0px;
        }

        .item-submit{
            margin-left: 88px;
        }

        input.error{
            border: 1px solid #E6594E;
        }

        input.highlight{
            border: 1px solid #7abd54;
        }

        label.error,label.tip{
            float: left;
            height: 32px;
            line-height: 32px;
            font-size: 14px;
            text-align: left;
            margin-left: 5px;
            padding-left: 20px;
            color: red;
            background: url('error.png') no-repeat left center;
        }

        label.tip{
            color: #aaa;
            background: url('tip.png') no-repeat left center;
        }

        label.valid{
            background: url('valid.png') no-repeat left center;
            width: 32px;
        }
    </style>
</head>
<body>
<form action="/login" method="post" id="loginForm">
    <div class="item">
        <label for="username" class="item-label">用户名:</label>
        <input type="text" id="username" name="username" class="item-text" placeholder="请输入用户名"
               autocomplete="off" tip="请输入用户名">
    </div>
    <div class="item">
        <label for="password" class="item-label">密码:</label>
        <input type="password" id="password" name="password" class="item-text" placeholder="请输入密码"
               tip="请输入密码">
    </div>
    <div class="item">
        <input type="submit" value="提交" class="item-submit">
    </div>
</form>
</body>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/jquery-validate/1.19.1/jquery.validate.min.js"></script>
<script>
    $(document).ready(function(){
        $("#loginForm").validate({
            rules: {
                username:{
                    required: true
                },
                password:{
                    required: true
                }
            },
            messages:{
                username:{
                    required: "用户名不能为空"
                },
                password:{
                    required: "密码不能为空"
                }
            }
        });
    });
</script>
</html>

8、添加功能:记住我

  • 配置 Shiro 中: 添加记住我功能
@Bean
public SimpleCookie rememberMeCookie(){
    SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
    simpleCookie.setMaxAge(259200);
    return simpleCookie;
}

@Bean
public CookieRememberMeManager rememberMeManager(){
    CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    cookieRememberMeManager.setCookie(rememberMeCookie());
    cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
    return cookieRememberMeManager;
}
  • 配置 Shiro 中: 修改拦截权限
map.put("/**","authc");

//修改为,根据业务需求定义拦截路径
map.put("/**","user");
  • 登录中: UsernamePasswordToke 中需要设置 rememberMe 属性
UserPasswdToken token = new UserPasswdToken(user.getUsername(), user.getPassword(), true);

9、添加功能:自定义密码规则

  • 配置 Shiro 中: 添加密码验证器
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    //指定加密方式为MD5
    credentialsMatcher.setHashAlgorithmName("MD5");
    //加密次数
    credentialsMatcher.setHashIterations(1024);
    credentialsMatcher.setStoredCredentialsHexEncoded(true);
    return credentialsMatcher;
}
  • 配置 Shiro 中: 自定义 Realm 设置密码验证器
@Bean
public UserRealm userRealm(){
    UserRealm realm = new UserRealm();
    realm.setCredentialsMatcher(hashedCredentialsMatcher());
    return realm;
}
  • 自定义 Realm中: 修改验证方式
//第一个参数:主体
//第二个参数:数据库密码
//第三个参数:盐
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,
user.getPassword(), ByteSource.Util.bytes(user.getUsername()), getName());
  • 密码生成方式
public static void main(String[] args) {
    String hashAlgorithName = "MD5";
    String password = "123456";
    int hashIterations = 1024;//加密次数
    ByteSource credentialsSalt = ByteSource.Util.bytes("admin");
    Object obj = new SimpleHash(hashAlgorithName, password, credentialsSalt, hashIterations);
    System.out.println(obj);
}

10、添加功能:配置 ecache 缓存

  • 配置 ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" dynamicConfig="false">
    <diskStore path="java.io.tmpdir"/>

    <cache name="users"
           timeToLiveSeconds="300"
           maxEntriesLocalHeap="1000"/>
    <defaultCache name="defaultCache"
                  maxElementsInMemory="10000"
                  eternal="false"
                  timeToIdleSeconds="120"
                  timeToLiveSeconds="120"
                  overflowToDisk="false"
                  maxElementsOnDisk="100000"
                  diskPersistent="false"
                  diskExpiryThreadIntervalSeconds="120"
                  memoryStoreEvictionPolicy="LRU"/>
</ehcache>
  • 配置 Shiro 中: 添加缓存管理器
@Bean
public EhCacheManager ehCacheManager(){
    EhCacheManager cacheManager = new EhCacheManager();
    cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
    return cacheManager;
}
  • 自定义 Realm中: 设置缓存管理器
@Bean
public UserRealm userRealm(){
    UserRealm realm = new UserRealm();
    realm.setCredentialsMatcher(hashedCredentialsMatcher());
    realm.setCacheManager(ehCacheManager());
    return realm;
}
  • 权限管理器中: 设置缓存管理器
@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(userRealm());
    securityManager.setRememberMeManager(rememberMeManager());
    securityManager.setCacheManager(ehCacheManager());
    return securityManager;
}

11、添加功能:限制登录错误次数

  • 重写 HashedCredentialsMatcher
package com.vim.common.shiro;

import com.vim.modules.web.model.User;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;

import java.util.concurrent.atomic.AtomicInteger;

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

    private Cache<String, AtomicInteger> passwordRetryCache;

    public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        UserPasswdToken user = (UserPasswdToken)token;
        AtomicInteger retryCount = passwordRetryCache.get(user.getUsername());
        //密码错误超过5次不允许登录
        if(retryCount == null) {
            retryCount = new AtomicInteger(0);
            passwordRetryCache.put(user.getUsername(), retryCount);
        }
        if(retryCount.incrementAndGet() > 5) {
            throw new ExcessiveAttemptsException();
        }

        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            passwordRetryCache.remove(user.getUsername());
        }
        return matches;
    }
}
  • 配置 Shiro 中: 密码验证器修改
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
    HashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher(ehCacheManager());
    //指定加密方式为MD5
    credentialsMatcher.setHashAlgorithmName("MD5");
    //加密次数
    credentialsMatcher.setHashIterations(1024);
    credentialsMatcher.setStoredCredentialsHexEncoded(true);
    return credentialsMatcher;
}

12、添加功能:设置 session 失效时间

  • 配置 Shiro 中: 配置 session 管理器
public DefaultWebSessionManager sessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    // 设置session过期时间5s
    sessionManager.setGlobalSessionTimeout(5000L);
    return sessionManager;
}
  • 配置 Shiro 中: 设置 session 管理器
@Bean
public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(userRealm());
    securityManager.setRememberMeManager(rememberMeManager());
    securityManager.setCacheManager(ehCacheManager());
    securityManager.setSessionManager(sessionManager());
    return securityManager;
}

13、添加功能:session 超时拦截器

  • 编写拦截器
package com.vim.common.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;

public class UserFilter extends AccessControlFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        if (isLoginRequest(request, response)) {
            return true;
        } else {
            Subject subject = getSubject(request, response);
            return subject.getPrincipal() != null;
        }
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        Subject subject = getSubject(request, response);
        httpResponse.setStatus(200);
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = null;
        HashMap<String, Object> result = new HashMap<>();
        try {
            out = httpResponse.getWriter();
            if (subject.getPrincipal() == null || !subject.isAuthenticated()) {
                result.put("result",0);
                result.put("code",99);
                result.put("msg","您尚未登录或登录时间过长,请重新登录!");
            } else {
                result.put("result",0);
                result.put("code",99);
                result.put("msg","您没有足够的权限执行该操作!");
            }
            out.print(result.toString());
        }catch(IOException e){
            e.printStackTrace();
        }finally {
            if (out != null) {
                out.close();
            }
        }
        return false;
    }
}
  • 配置 Shiro 中: 配置拦截器
//1.配置拦截器拦截路径
map.put("/api/**", "userFilter");

//2.自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("userFilter", new UserFilter());
shiroFilterFactoryBean.setFilters(filtersMap);

14、集成 cas 服务器

  • 配置依赖
<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-cas</artifactId>
  <version>1.2.3</version>
  <exclusions>
    <exclusion>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
    </exclusion>
  </exclusions>
</dependency>
  • 配置地址
// CasServerUrlPrefix
public static final String casServerUrlPrefix = "http://localhost:8443/cas";
// Cas登录页面地址
public static final String casLoginUrl = casServerUrlPrefix + "/login";
// Cas登出页面地址
public static final String casLogoutUrl = casServerUrlPrefix + "/logout";
// 当前工程对外提供的服务地址
public static final String shiroServerUrlPrefix = "http://localhost:8081";
// casFilter UrlPattern
public static final String casFilterUrlPattern = "/shiro-cas";
// 登录地址
public static final String loginUrl = casLoginUrl + "?service=" + shiroServerUrlPrefix + casFilterUrlPattern;
  • 定义 casFilter
@Bean
public CasFilter casFilter(){
    CasFilter casFilter = new CasFilter();
    casFilter.setName("casFilter");
    casFilter.setEnabled(true);
    casFilter.setFailureUrl(loginUrl);
    return casFilter;
}
  • 重新配置 ShiroFilterFactoryBean
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager());

    //修改1
    shiroFilterFactoryBean.setLoginUrl(loginUrl);
    shiroFilterFactoryBean.setSuccessUrl("/test");

    Map<String,String> map = new HashMap<>();
    //修改2
    map.put(casFilterUrlPattern,"casFilter");

    map.put("/login","anon");
    map.put("/logout","anon");

    map.put("/**","anon");

    Map<String, Filter> filtersMap = new LinkedHashMap<>();
    //修改3
    filtersMap.put("casFilter", casFilter());
    shiroFilterFactoryBean.setFilters(filtersMap);

    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    return shiroFilterFactoryBean;
}
  • 修改 Realm 实现方式
package com.vim.common.shiro;

import com.vim.common.config.ShiroConfig;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cas.CasRealm;
import org.apache.shiro.subject.PrincipalCollection;

import javax.annotation.PostConstruct;

public class UserRealm extends CasRealm {

    @PostConstruct
    public void initUrl(){
        setCasServerUrlPrefix(ShiroConfig.casServerUrlPrefix);
        // 客户端回调地址
        setCasService(ShiroConfig.shiroServerUrlPrefix + ShiroConfig.casFilterUrlPattern);
    }

    /**
     * 权限校验
     * @param principal
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //1.取出主体信息
        String user= (String) principal.getPrimaryPrincipal();
        System.out.println(user);
        //2.查询权限信息

        //3.添加权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermission("sys:user:list");
        simpleAuthorizationInfo.addStringPermission("sys:user:edit");
        return simpleAuthorizationInfo;
    }

    /**
     * 用户名和密码校验
     * @param token
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo authc = super.doGetAuthenticationInfo(token);
        return authc;
    }
}

猜你喜欢

转载自blog.csdn.net/sky_eyeland/article/details/93977735
今日推荐