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、添加功能:记住我
@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;
}
map.put("/**","authc");
//修改为,根据业务需求定义拦截路径
map.put("/**","user");
- 登录中: UsernamePasswordToke 中需要设置 rememberMe 属性
UserPasswdToken token = new UserPasswdToken(user.getUsername(), user.getPassword(), true);
9、添加功能:自定义密码规则
@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;
}
//第一个参数:主体
//第二个参数:数据库密码
//第三个参数:盐
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 缓存
<?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>
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
@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;
}
}
@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;
}
}
//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;
@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;
}
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;
}
}