准备内容

简单的shiro无状态认证

  无状态认证拦截器

import com.hjzgg.stateless.shiroSimpleWeb.Constants;
import com.hjzgg.stateless.shiroSimpleWeb.realm.StatelessToken;
import org.apache.shiro.web.filter.AccessControlFilter; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map; /** * <p>Version: 1.0
*/
public class StatelessAuthcFilter extends AccessControlFilter { @Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
} @Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//1、客户端生成的消息摘要
String clientDigest = request.getParameter(Constants.PARAM_DIGEST);
//2、客户端传入的用户身份
String username = request.getParameter(Constants.PARAM_USERNAME);
//3、客户端请求的参数列表
Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap());
params.remove(Constants.PARAM_DIGEST); //4、生成无状态Token
StatelessToken token = new StatelessToken(username, params, clientDigest); try {
//5、委托给Realm进行登录
getSubject(request, response).login(token);
} catch (Exception e) {
e.printStackTrace();
onLoginFail(response); //6、登录失败
return false;
}
return true;
} //登录失败时默认返回401状态码
private void onLoginFail(ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("login error");
}
}

  Subject工厂

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory; /** * <p>Version: 1.0
*/
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}

  注意,这里禁用了session

  无状态Realm

import com.hjzgg.stateless.shiroSimpleWeb.codec.HmacSHA256Utils;
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; /** * <p>Version: 1.0
*/
public class StatelessRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
//仅支持StatelessToken类型的Token
return token instanceof StatelessToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//根据用户名查找角色,请根据需求实现
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRole("admin");
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
StatelessToken statelessToken = (StatelessToken) token;
String username = statelessToken.getUsername();
String key = getKey(username);//根据用户名获取密钥(和客户端的一样)
//在服务器端生成客户端参数消息摘要
String serverDigest = HmacSHA256Utils.digest(key, statelessToken.getParams());
System.out.println(statelessToken.getClientDigest());
System.out.println(serverDigest);
//然后进行客户端消息摘要和服务器端消息摘要的匹配
return new SimpleAuthenticationInfo(
username,
serverDigest,
getName());
} private String getKey(String username) {//得到密钥,此处硬编码一个
if("admin".equals(username)) {
return "dadadswdewq2ewdwqdwadsadasd";
}
return null;
}
}

  无状态Token

import org.apache.shiro.authc.AuthenticationToken;
import org.springframework.beans.*;
import org.springframework.validation.DataBinder; import java.util.HashMap;
import java.util.Map; /** * <p>Version: 1.0
*/
public class StatelessToken implements AuthenticationToken { private String username;
private Map<String, ?> params;
private String clientDigest; public StatelessToken(String username, Map<String, ?> params, String clientDigest) {
this.username = username;
this.params = params;
this.clientDigest = clientDigest;
} public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public Map<String, ?> getParams() {
return params;
} public void setParams( Map<String, ?> params) {
this.params = params;
} public String getClientDigest() {
return clientDigest;
} public void setClientDigest(String clientDigest) {
this.clientDigest = clientDigest;
} @Override
public Object getPrincipal() {
return username;
} @Override
public Object getCredentials() {
return clientDigest;
} public static void main(String[] args) { }
public static void test1() {
StatelessToken token = new StatelessToken(null, null, null);
BeanWrapperImpl beanWrapper = new BeanWrapperImpl(token);
beanWrapper.setPropertyValue(new PropertyValue("username", "hjzgg"));
System.out.println(token.getUsername());
} public static void test2() {
StatelessToken token = new StatelessToken(null, null, null);
DataBinder dataBinder = new DataBinder(token);
Map<String, Object> params = new HashMap<>();
params.put("username", "hjzgg");
PropertyValues propertyValues = new MutablePropertyValues(params);
dataBinder.bind(propertyValues);
System.out.println(token.getUsername());
}
}

  shiro配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- Realm实现 -->
<bean id="statelessRealm" class="com.hjzgg.stateless.shiroSimpleWeb.realm.StatelessRealm">
<property name="cachingEnabled" value="false"/>
</bean> <!-- Subject工厂 -->
<bean id="subjectFactory" class="com.hjzgg.stateless.shiroSimpleWeb.mgt.StatelessDefaultSubjectFactory"/> <!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false"/>
</bean> <!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="statelessRealm"/>
<property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled" value="false"/>
<property name="subjectFactory" ref="subjectFactory"/>
<property name="sessionManager" ref="sessionManager"/>
</bean> <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean> <bean id="statelessAuthcFilter" class="com.hjzgg.stateless.shiroSimpleWeb.filter.StatelessAuthcFilter"/> <!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="filters">
<util:map>
<entry key="statelessAuthc" value-ref="statelessAuthcFilter"/>
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
/**=statelessAuthc
</value>
</property>
</bean> <!-- Shiro生命周期处理器-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>

  这里禁用了回话调度器的session存储

  web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="false"> <display-name>shiro-example-chapter20</display-name> <!-- Spring配置文件开始 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:spring-config-shiro.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring配置文件结束 --> <!-- shiro 安全过滤器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter> <filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping> <servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping> </web-app>

  token生成工具类

import org.apache.commons.codec.binary.Hex;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.List;
import java.util.Map; /** * <p>Version: 1.0
*/
public class HmacSHA256Utils { public static String digest(String key, String content) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
byte[] secretByte = key.getBytes("utf-8");
byte[] dataBytes = content.getBytes("utf-8"); SecretKey secret = new SecretKeySpec(secretByte, "HMACSHA256");
mac.init(secret); byte[] doFinal = mac.doFinal(dataBytes);
byte[] hexB = new Hex().encode(doFinal);
return new String(hexB, "utf-8");
} catch (Exception e) {
throw new RuntimeException(e);
}
} public static String digest(String key, Map<String, ?> map) {
StringBuilder s = new StringBuilder();
for(Object values : map.values()) {
if(values instanceof String[]) {
for(String value : (String[])values) {
s.append(value);
}
} else if(values instanceof List) {
for(String value : (List<String>)values) {
s.append(value);
}
} else {
s.append(values);
}
}
return digest(key, s.toString());
} }

  简单测试一下

import com.alibaba.fastjson.JSONObject;
import com.hjzgg.stateless.shiroSimpleWeb.codec.HmacSHA256Utils;
import com.hjzgg.stateless.shiroSimpleWeb.utils.RestTemplateUtils;
import org.junit.Test;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder; /**
* <p>Version: 1.0
*/
public class ClientTest { private static final String WEB_URL = "http://localhost:8080/shiro/hello"; @Test
public void testServiceHelloSuccess() {
String username = "admin";
String param11 = "param11";
String param12 = "param12";
String param2 = "param2";
String key = "dadadswdewq2ewdwqdwadsadasd";
JSONObject params = new JSONObject();
params.put(Constants.PARAM_USERNAME, username);
params.put("param1", param11);
params.put("param1", param12);
params.put("param2", param2);
params.put(Constants.PARAM_DIGEST, HmacSHA256Utils.digest(key, params)); String result = RestTemplateUtils.get(WEB_URL, params);
System.out.println(result);
} @Test
public void testServiceHelloFail() {
String username = "admin";
String param11 = "param11";
String param12 = "param12";
String param2 = "param2";
String key = "dadadswdewq2ewdwqdwadsadasd";
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
params.add(Constants.PARAM_USERNAME, username);
params.add("param1", param11);
params.add("param1", param12);
params.add("param2", param2);
params.add(Constants.PARAM_DIGEST, HmacSHA256Utils.digest(key, params));
params.set("param2", param2 + "1"); String url = UriComponentsBuilder
.fromHttpUrl("http://localhost:8080/hello")
.queryParams(params).build().toUriString();
}
}

  补充Spring中多重属性赋值处理

  以上参考 开涛老师的博文

相对复杂一点的shiro无状态认证

  *加入session,放入redis中(user_name作为key值,token作为hash值,当前登录时间作为value值)

  *用户登录互斥操作:如果互斥,清除redis中该用户对应的状态,重新写入新的状态;如果不互斥,写入新的状态,刷新key值,并检测该用户其他的状态是否已经超时(根据key值获取到所有的 key和hashKey的组合,判断value[登入时间]+timeout[超时时间] >= curtime[当前时间]),如果超时则清除状态。

  *使用esapi进行token的生成

  *认证信息,如果是web端则从cookie中获取,ajax从header中获取;如果是移动端也是从header中获取

  session manager逻辑

import com.hjzgg.stateless.auth.token.ITokenProcessor;
import com.hjzgg.stateless.auth.token.TokenFactory;
import com.hjzgg.stateless.auth.token.TokenGenerator;
import com.hjzgg.stateless.common.cache.RedisCacheTemplate;
import com.hjzgg.stateless.common.esapi.EncryptException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; @Component
public class ShiroSessionManager { @Autowired
private RedisCacheTemplate redisCacheTemplate; @Value("${sessionMutex}")
private boolean sessionMutex = false; public static final String TOKEN_SEED = "token_seed"; public static final String DEFAULT_CHARSET = "UTF-8"; private final Logger logger = LoggerFactory.getLogger(getClass()); private static String localSeedValue = null; /**
* 获得当前系统的 token seed
*/
public String findSeed() throws EncryptException {
if(localSeedValue != null){
return localSeedValue;
} else {
String seed = getSeedValue(TOKEN_SEED);
if (StringUtils.isBlank(seed)) {
seed = TokenGenerator.genSeed();
localSeedValue = seed;
redisCacheTemplate.put(TOKEN_SEED, seed);
}
return seed;
}
} public String getSeedValue(String key) {
return (String) redisCacheTemplate.get(key);
} /**
* 删除session缓存
*
* @param sid mock的sessionid
*/
public void removeSessionCache(String sid) {
redisCacheTemplate.delete(sid);
} private int getTimeout(String sid){
return TokenFactory.getTokenInfo(sid).getIntegerExpr();
} private String getCurrentTimeSeconds() {
return String.valueOf(System.currentTimeMillis()/1000);
} public void registOnlineSession(final String userName, final String token, final ITokenProcessor processor) {
final String key = userName;
logger.debug("token processor id is {}, key is {}, sessionMutex is {}!" , processor.getId(), key, sessionMutex); // 是否互斥,如果是,则踢掉所有当前用户的session,重新创建,此变量将来从配置文件读取
if(sessionMutex){
deleteUserSession(key);
} else {
// 清理此用户过期的session,过期的常为异常或者直接关闭浏览器,没有走正常注销的key
clearOnlineSession(key);
} redisCacheTemplate.hPut(userName, token, getCurrentTimeSeconds());
int timeout = getTimeout(token);
if (timeout > 0) {
redisCacheTemplate.expire(token, timeout);
}
} private void clearOnlineSession(final String key) {
redisCacheTemplate.hKeys(key).forEach((obj) -> {
String hashKey = (String) obj;
int timeout = getTimeout(hashKey);
if (timeout > 0) {
int oldTimeSecondsValue = Integer.valueOf((String) redisCacheTemplate.hGet(key, hashKey));
int curTimeSecondsValue = (int) (System.currentTimeMillis()/1000);
//如果 key-hashKey 对应的时间+过期时间 小于 当前时间,则剔除
if(curTimeSecondsValue - (oldTimeSecondsValue+timeout) > 0) {
redisCacheTemplate.hDel(key, hashKey);
}
}
});
} public boolean validateOnlineSession(final String key, final String hashKey) {
int timeout = getTimeout(hashKey);
if (timeout > 0) {
String oldTimeSecondsValue = (String) redisCacheTemplate.hGet(key, hashKey);
if (StringUtils.isEmpty(oldTimeSecondsValue)) {
return false;
} else {
int curTimeSecondsValue = (int) (System.currentTimeMillis()/1000);
if(Integer.valueOf(oldTimeSecondsValue)+timeout >= curTimeSecondsValue) {
//刷新 key
redisCacheTemplate.hPut(key, hashKey, getCurrentTimeSeconds());
redisCacheTemplate.expire(key, timeout);
return true;
} else {
redisCacheTemplate.hDel(key, hashKey);
return false;
}
}
} else {
return redisCacheTemplate.hGet(key, hashKey) != null;
}
} // 注销用户时候需要调用
public void delOnlineSession(final String key, final String hashKey){
redisCacheTemplate.hDel(key, hashKey);
} // 禁用或者删除用户时候调用
public void deleteUserSession(final String key){
redisCacheTemplate.delete(key);
}
}

  无状态认证过滤器

package com.hjzgg.stateless.auth.shiro;

import com.alibaba.fastjson.JSONObject;
import com.hjzgg.stateless.auth.token.ITokenProcessor;
import com.hjzgg.stateless.auth.token.TokenFactory;
import com.hjzgg.stateless.auth.token.TokenParameter;
import com.hjzgg.stateless.common.constants.AuthConstants;
import com.hjzgg.stateless.common.utils.CookieUtil;
import com.hjzgg.stateless.common.utils.InvocationInfoProxy;
import com.hjzgg.stateless.common.utils.MapToStringUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.*; public class StatelessAuthcFilter extends AccessControlFilter { private static final Logger log = LoggerFactory.getLogger(StatelessAuthcFilter.class); public static final int HTTP_STATUS_AUTH = 306; @Value("${filterExclude}")
private String exeludeStr; @Autowired
private TokenFactory tokenFactory; private String[] esc = new String[] {
"/logout","/login","/formLogin",".jpg",".png",".gif",".css",".js",".jpeg"
}; private List<String> excludCongtextKeys = new ArrayList<>(); public void setTokenFactory(TokenFactory tokenFactory) {
this.tokenFactory = tokenFactory;
} public void setEsc(String[] esc) {
this.esc = esc;
} public void setExcludCongtextKeys(List<String> excludCongtextKeys) {
this.excludCongtextKeys = excludCongtextKeys;
} @Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
} @Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { boolean isAjax = isAjax(request); // 1、客户端发送来的摘要
HttpServletRequest hReq = (HttpServletRequest) request;
HttpServletRequest httpRequest = hReq;
Cookie[] cookies = httpRequest.getCookies();
String authority = httpRequest.getHeader("Authority"); //如果header中包含,则以header为主,否则,以cookie为主
if(StringUtils.isNotBlank(authority)){
Set<Cookie> cookieSet = new HashSet<Cookie>();
String[] ac = authority.split(";");
for(String s : ac){
String[] cookieArr = s.split("=");
String key = StringUtils.trim(cookieArr[0]);
String value = StringUtils.trim(cookieArr[1]);
Cookie cookie = new Cookie(key, value);
cookieSet.add(cookie);
}
cookies = cookieSet.toArray(new Cookie[]{});
} String tokenStr = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_TOKEN);
String cookieUserName = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_USERNAME); String loginTs = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_LOGINTS); // 2、客户端传入的用户身份
String userName = request.getParameter(AuthConstants.PARAM_USERNAME);
if (userName == null && StringUtils.isNotBlank(cookieUserName)) {
userName = cookieUserName;
} boolean needCheck = !include(hReq); if (needCheck) {
if (StringUtils.isEmpty(tokenStr) || StringUtils.isEmpty(userName)) {
if (isAjax) {
onAjaxAuthFail(request, response);
} else {
onLoginFail(request, response);
}
return false;
} // 3、客户端请求的参数列表
Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap()); ITokenProcessor tokenProcessor = tokenFactory.getTokenProcessor(tokenStr);
TokenParameter tp = tokenProcessor.getTokenParameterFromCookie(cookies);
// 4、生成无状态Token
StatelessToken token = new StatelessToken(userName, tokenProcessor, tp, params, new String(tokenStr)); try {
// 5、委托给Realm进行登录
getSubject(request, response).login(token); // 这个地方应该验证上下文信息中的正确性 // 设置上下文变量
InvocationInfoProxy.setUserName(userName);
InvocationInfoProxy.setLoginTs(loginTs);
InvocationInfoProxy.setToken(tokenStr); //设置上下文携带的额外属性
initExtendParams(cookies); initMDC();
afterValidate(hReq);
} catch (Exception e) {
log.error(e.getMessage(), e);
if (isAjax && e instanceof AuthenticationException) {
onAjaxAuthFail(request, response); // 6、验证失败,返回ajax调用方信息
return false;
} else {
onLoginFail(request, response); // 6、登录失败,跳转到登录页
return false;
}
}
return true;
} else {
return true;
} } private boolean isAjax(ServletRequest request) {
boolean isAjax = false;
if (request instanceof HttpServletRequest) {
HttpServletRequest rq = (HttpServletRequest) request;
String requestType = rq.getHeader("X-Requested-With");
if (requestType != null && "XMLHttpRequest".equals(requestType)) {
isAjax = true;
}
}
return isAjax;
} protected void onAjaxAuthFail(ServletRequest request, ServletResponse resp) throws IOException {
HttpServletResponse response = (HttpServletResponse) resp;
JSONObject json = new JSONObject();
json.put("msg", "auth check error!");
response.setStatus(HTTP_STATUS_AUTH);
response.getWriter().write(json.toString());
} // 登录失败时默认返回306状态码
protected void onLoginFail(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HTTP_STATUS_AUTH);
request.setAttribute("msg", "auth check error!");
// 跳转到登录页
redirectToLogin(request, httpResponse);
} @Override
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest hReq = (HttpServletRequest) request;
String rURL = hReq.getRequestURI();
String errors = StringUtils.isEmpty((String) request.getAttribute("msg")) ? "" : "&msg=" + request.getAttribute("msg"); if(request.getAttribute("msg") != null) {
rURL += ((StringUtils.isNotEmpty(hReq.getQueryString())) ?
"&" : "") + "msg=" + request.getAttribute("msg");
} rURL = Base64.encodeBase64URLSafeString(rURL.getBytes()) ;
// 加入登录前地址, 以及错误信息
String loginUrl = getLoginUrl() + "?r=" + rURL + errors; WebUtils.issueRedirect(request, response, loginUrl);
} public boolean include(HttpServletRequest request) {
String u = request.getRequestURI();
for (String e : esc) {
if (u.endsWith(e)) {
return true;
}
} if(StringUtils.isNotBlank(exeludeStr)){
String[] customExcludes = exeludeStr.split(",");
for (String e : customExcludes) {
if (u.endsWith(e)) {
return true;
}
}
} return false;
} @Override
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
super.afterCompletion(request, response, exception);
InvocationInfoProxy.reset();
clearMDC();
} // 设置上下文中的扩展参数,rest传递上下文时生效,Authority header中排除固定key的其它信息都设置到InvocationInfoProxy的parameters
private void initExtendParams(Cookie[] cookies) {
for (Cookie cookie : cookies) {
String cname = cookie.getName();
String cvalue = cookie.getValue();
if(!excludCongtextKeys.contains(cname)){
InvocationInfoProxy.setParameter(cname, cvalue);
}
}
} private void initMDC() {
String userName = "";
Subject subject = SecurityUtils.getSubject();
if (subject != null && subject.getPrincipal() != null) {
userName = (String) SecurityUtils.getSubject().getPrincipal();
} // MDC中记录用户信息
MDC.put(AuthConstants.PARAM_USERNAME, userName); initCustomMDC();
} protected void initCustomMDC() {
MDC.put("InvocationInfoProxy", MapToStringUtil.toEqualString(InvocationInfoProxy.getResources(), ';'));
} protected void afterValidate(HttpServletRequest hReq){
} protected void clearMDC() {
// MDC中记录用户信息
MDC.remove(AuthConstants.PARAM_USERNAME); clearCustomMDC();
} protected void clearCustomMDC() {
MDC.remove("InvocationInfoProxy");
} //初始化 AuthConstants类中定义的常量
{
Field[] fields = AuthConstants.class.getDeclaredFields();
try {
for (Field field : fields) {
field.setAccessible(true);
if (field.getType().toString().endsWith("java.lang.String")
&& Modifier.isStatic(field.getModifiers())) {
excludCongtextKeys.add((String) field.get(AuthConstants.class));
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

  dubbo服务调用时上下文的传递问题

  思路:认证过滤器中 通过MDC将上下文信息写入到InheritableThreadLocal中,写一个dubbo的过滤器。在过滤器中判断,如果是消费一方,则将MDC中的上下文取出来放入dubbo的context变量中;如果是服务方,则从dubbo的context中拿出上下文,解析并放入MDC以及InvocationInfoProxy(下面会提到)类中

  Subject工厂

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory; public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}

  同样禁用掉session的创建

  无状态Realm

import com.hjzgg.stateless.auth.session.ShiroSessionManager;
import com.hjzgg.stateless.auth.token.ITokenProcessor;
import com.hjzgg.stateless.auth.token.TokenParameter;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList;
import java.util.List; public class StatelessRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(StatelessRealm.class); @Autowired
private ShiroSessionManager shiroSessionManager; @Override
public boolean supports(AuthenticationToken token) {
// 仅支持StatelessToken类型的Token
return token instanceof StatelessToken;
} @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<String> roles = new ArrayList<String>();
info.addRoles(roles);
return info;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken atoken) throws AuthenticationException {
StatelessToken token = (StatelessToken) atoken;
TokenParameter tp = token.getTp();
String userName = (String) token.getPrincipal();
ITokenProcessor tokenProcessor = token.getTokenProcessor();
String tokenStr = tokenProcessor.generateToken(tp);
if (tokenStr == null || !shiroSessionManager.validateOnlineSession(userName, tokenStr)) {
logger.error("User [{}] authenticate fail in System, maybe session timeout!", userName);
throw new AuthenticationException("User " + userName + " authenticate fail in System");
} return new SimpleAuthenticationInfo(userName, tokenStr, getName());
} }

View Code

  这里使用自定义 session manager去校验

  无状态token

import com.hjzgg.stateless.auth.token.ITokenProcessor;
import com.hjzgg.stateless.auth.token.TokenParameter;
import org.apache.shiro.authc.AuthenticationToken; import java.util.Map; public class StatelessToken implements AuthenticationToken { private String userName;
// 预留参数集合,校验更复杂的权限
private Map<String, ?> params;
private String clientDigest;
ITokenProcessor tokenProcessor;
TokenParameter tp;
public StatelessToken(String userName, ITokenProcessor tokenProcessor, TokenParameter tp , Map<String, ?> params, String clientDigest) {
this.userName = userName;
this.params = params;
this.tp = tp;
this.tokenProcessor = tokenProcessor;
this.clientDigest = clientDigest;
} public TokenParameter getTp() {
return tp;
} public void setTp(TokenParameter tp) {
this.tp = tp;
} public String getUserName() {
return userName;
} public void setUserName(String userName) {
this.userName = userName;
} public Map<String, ?> getParams() {
return params;
} public void setParams( Map<String, ?> params) {
this.params = params;
} public String getClientDigest() {
return clientDigest;
} public void setClientDigest(String clientDigest) {
this.clientDigest = clientDigest;
} @Override
public Object getPrincipal() {
return userName;
} @Override
public Object getCredentials() {
return clientDigest;
} public ITokenProcessor getTokenProcessor() {
return tokenProcessor;
} public void setTokenProcessor(ITokenProcessor tokenProcessor) {
this.tokenProcessor = tokenProcessor;
}
}

  token处理器

import com.hjzgg.stateless.auth.session.ShiroSessionManager;
import com.hjzgg.stateless.common.constants.AuthConstants;
import com.hjzgg.stateless.common.esapi.EncryptException;
import com.hjzgg.stateless.common.esapi.IYCPESAPI;
import com.hjzgg.stateless.common.utils.CookieUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.http.Cookie;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry; /**
* 默认Token处理器提供将cooke和TokenParameter相互转换,Token生成的能力
* <p>
* 可以注册多个实例
* </p>
*
* @author li
*
*/
public class DefaultTokenPorcessor implements ITokenProcessor {
private static Logger log = LoggerFactory.getLogger(DefaultTokenPorcessor.class);
private static int HTTPVERSION = 3;
static {
URL res = DefaultTokenPorcessor.class.getClassLoader().getResource("javax/servlet/annotation/WebServlet.class");
if (res == null) {
HTTPVERSION = 2;
}
}
private String id;
private String domain;
private String path = "/";
private Integer expr;
// 默认迭代次数
private int hashIterations = 2; @Autowired
private ShiroSessionManager shiroSessionManager; @Override
public String getId() {
return id;
} public void setId(String id) {
this.id = id;
} public String getDomain() {
return domain;
} public void setDomain(String domain) {
this.domain = domain;
} public String getPath() {
return path;
} public void setPath(String path) {
this.path = path;
} public Integer getExpr() {
return expr;
} public void setExpr(Integer expr) {
this.expr = expr;
} private List<String> exacts = new ArrayList<String>(); public void setExacts(List<String> exacts) {
this.exacts = exacts;
} public int getHashIterations() {
return hashIterations;
} public void setHashIterations(int hashIterations) {
this.hashIterations = hashIterations;
} @Override
public String generateToken(TokenParameter tp) {
try {
String seed = shiroSessionManager.findSeed();
String token = IYCPESAPI.encryptor().hash(
this.id + tp.getUserName() + tp.getLoginTs() + getSummary(tp) + getExpr(),
seed,
getHashIterations());
token = this.id + "," + getExpr() + "," + token;
return Base64.encodeBase64URLSafeString(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(token));
} catch (EncryptException e) {
log.error("TokenParameter is not validate!", e);
throw new IllegalArgumentException("TokenParameter is not validate!");
}
} @Override
public Cookie[] getCookieFromTokenParameter(TokenParameter tp) {
List<Cookie> cookies = new ArrayList<Cookie>();
String tokenStr = generateToken(tp);
Cookie token = new Cookie(AuthConstants.PARAM_TOKEN, tokenStr);
if (HTTPVERSION == 3)
token.setHttpOnly(true);
if (StringUtils.isNotEmpty(domain))
token.setDomain(domain);
token.setPath(path);
cookies.add(token); try {
Cookie userId = new Cookie(AuthConstants.PARAM_USERNAME, URLEncoder.encode(tp.getUserName(), "UTF-8"));
if (StringUtils.isNotEmpty(domain))
userId.setDomain(domain);
userId.setPath(path);
cookies.add(userId); // 登录的时间戳
Cookie logints = new Cookie(AuthConstants.PARAM_LOGINTS, URLEncoder.encode(tp.getLoginTs(), "UTF-8"));
if (StringUtils.isNotEmpty(domain))
logints.setDomain(domain);
logints.setPath(path);
cookies.add(logints);
} catch (UnsupportedEncodingException e) {
log.error("encode error!", e);
} if (!tp.getExt().isEmpty()) {
Iterator<Entry<String, String>> it = tp.getExt().entrySet().iterator();
while (it.hasNext()) {
Entry<String, String> i = it.next();
Cookie ext = new Cookie(i.getKey(), i.getValue());
if (StringUtils.isNotEmpty(domain))
ext.setDomain(domain);
ext.setPath(path);
cookies.add(ext);
}
} shiroSessionManager.registOnlineSession(tp.getUserName(), tokenStr, this); return cookies.toArray(new Cookie[] {});
} @Override
public TokenParameter getTokenParameterFromCookie(Cookie[] cookies) {
TokenParameter tp = new TokenParameter();
String token = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_TOKEN);
TokenInfo ti = TokenFactory.getTokenInfo(token);
if (ti.getIntegerExpr().intValue() != this.getExpr().intValue()) {
throw new IllegalArgumentException("illegal token!");
}
String userId = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_USERNAME);
tp.setUserName(userId);
String loginTs = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_LOGINTS);
tp.setLoginTs(loginTs); if (exacts != null && !exacts.isEmpty()) {
for (int i = 0; i < cookies.length; i++) {
Cookie cookie = cookies[i];
String name = cookie.getName();
if (exacts.contains(name)) {
tp.getExt().put(name,
cookie.getValue() == null ? "" : cookie.getValue());
}
}
}
return tp;
} protected String getSummary(TokenParameter tp) {
if (exacts != null && !exacts.isEmpty()) {
int len = exacts.size();
String[] exa = new String[len];
for (int i = 0; i < len; i++) {
String name = exacts.get(i);
String value = tp.getExt().get(name);
if(value == null) value = "";
exa[i] = value;
}
return StringUtils.join(exa, "#");
}
return "";
} @Override
public Cookie[] getLogoutCookie(String tokenStr, String uid) {
List<Cookie> cookies = new ArrayList<Cookie>();
Cookie token = new Cookie(AuthConstants.PARAM_TOKEN, null);
if (StringUtils.isNotEmpty(domain))
token.setDomain(domain);
token.setPath(path);
cookies.add(token); Cookie userId = new Cookie(AuthConstants.PARAM_USERNAME, null);
if (StringUtils.isNotEmpty(domain))
userId.setDomain(domain);
userId.setPath(path);
cookies.add(userId); // 登录的时间戳
Cookie logints = new Cookie(AuthConstants.PARAM_LOGINTS, null);
if (StringUtils.isNotEmpty(domain))
logints.setDomain(domain);
logints.setPath(path);
cookies.add(logints);
for (String exact : exacts) {
Cookie ext = new Cookie(exact, null);
if (StringUtils.isNotEmpty(domain))
ext.setDomain(domain);
ext.setPath(path);
cookies.add(ext);
} shiroSessionManager.delOnlineSession(uid, tokenStr); return cookies.toArray(new Cookie[] {});
}
}

  将一些必须字段和扩展字段进行通过esapi 的hash算法进行加密,生成token串,最终的token = token处理器标识+过期时间+原token

  shiro配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="statelessRealm" class="com.hjzgg.stateless.auth.shiro.StatelessRealm">
<property name="cachingEnabled" value="false" />
</bean> <!-- Subject工厂 -->
<bean id="subjectFactory"
class="com.hjzgg.stateless.auth.shiro.StatelessDefaultSubjectFactory" /> <bean id="webTokenProcessor" class="com.hjzgg.stateless.auth.token.DefaultTokenPorcessor">
<property name="id" value="web"></property>
<property name="path" value="${context.name}"></property>
<property name="expr" value="${sessionTimeout}"></property>
<property name="exacts">
<list>
<value type="java.lang.String">userType</value>
</list>
</property>
</bean>
<bean id="maTokenProcessor" class="com.hjzgg.stateless.auth.token.DefaultTokenPorcessor">
<property name="id" value="ma"></property>
<property name="path" value="${context.name}"></property>
<property name="expr" value="-1"></property>
<property name="exacts">
<list>
<value type="java.lang.String">userType</value>
</list>
</property>
</bean> <bean id="tokenFactory" class="com.hjzgg.stateless.auth.token.TokenFactory">
<property name="processors">
<list>
<ref bean="webTokenProcessor" />
<ref bean="maTokenProcessor" />
</list>
</property>
</bean> <!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false" />
</bean> <!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="statelessRealm" />
</list>
</property>
<property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled"
value="false" />
<property name="subjectFactory" ref="subjectFactory" />
<property name="sessionManager" ref="sessionManager" />
</bean> <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod"
value="org.apache.shiro.SecurityUtils.setSecurityManager" />
<property name="arguments" ref="securityManager" />
</bean> <bean id="statelessAuthcFilter" class="com.hjzgg.stateless.auth.shiro.StatelessAuthcFilter">
<property name="tokenFactory" ref="tokenFactory" />
</bean> <bean id="logout" class="com.hjzgg.stateless.auth.shiro.LogoutFilter"></bean> <!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login" />
<property name="filters">
<util:map>
<entry key="statelessAuthc" value-ref="statelessAuthcFilter" />
</util:map>
</property>
<property name="filterChainDefinitions">
<value>
<!--swagger-->
/webjars/** = anon
/v2/api-docs/** = anon
/swagger-resources/** = anon /login/** = anon
/logout = logout
/static/** = anon
/css/** = anon
/images/** = anon
/trd/** = anon
/js/** = anon
/api/** = anon
/cxf/** = anon
/jaxrs/** = anon
/** = statelessAuthc
</value>
</property>
</bean>
<!-- Shiro生命周期处理器 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>

  通过InvocationInfoProxy这个类(基于ThreadLocal的),可以拿到用户相关的参数信息

import com.hjzgg.stateless.common.constants.AuthConstants;

import java.util.HashMap;
import java.util.Map; /**
* Created by hujunzheng on 2017/7/18.
*/
public class InvocationInfoProxy {
private static final ThreadLocal<Map<String, Object>> resources =
ThreadLocal.withInitial(() -> {
Map<String, Object> initialValue = new HashMap<>();
initialValue.put(AuthConstants.ExtendConstants.PARAM_PARAMETER, new HashMap<String, String>());
return initialValue;
}
); public static String getUserName() {
return (String) resources.get().get(AuthConstants.PARAM_USERNAME);
} public static void setUserName(String userName) {
resources.get().put(AuthConstants.PARAM_USERNAME, userName);
} public static String getLoginTs() {
return (String) resources.get().get(AuthConstants.PARAM_LOGINTS);
} public static void setLoginTs(String loginTs) {
resources.get().put(AuthConstants.PARAM_LOGINTS, loginTs);
} public static String getToken() {
return (String) resources.get().get(AuthConstants.PARAM_TOKEN);
} public static void setToken(String token) {
resources.get().put(AuthConstants.PARAM_TOKEN, token);
} public static void setParameter(String key, String value) {
((Map<String, String>) resources.get().get(AuthConstants.ExtendConstants.PARAM_PARAMETER)).put(key, value);
} public static String getParameter(String key) {
return ((Map<String, String>) resources.get().get(AuthConstants.ExtendConstants.PARAM_PARAMETER)).get(key);
} public static void reset() {
resources.remove();
}
}

  还有esapi和cache的相关代码到项目里看一下吧

项目地址

  欢迎访问,无状态shiro认证组件

参考拦截

    ESAPI入门使用方法

   Spring MVC 4.2 增加 CORS 支持

  HTTP访问控制(CORS)

  Slf4j MDC 使用和 基于 Logback 的实现分析

无状态shiro认证组件(禁用默认session)的更多相关文章

  1. ASP.NET Core的无状态身份认证框架IdentityServer4

    Identity Server 4是IdentityServer的最新版本,它是流行的OpenID Connect和OAuth Framework for .NET,为ASP.NET Core和.NE ...

  2. shiro jwt 构建无状态分布式鉴权体系

    一:JWT 1.令牌构造 JWT(json web token)是可在网络上传输的用于声明某种主张的令牌(token),以JSON 对象为载体的轻量级开放标准(RFC 7519). 一个JWT令牌的定 ...

  3. Django--用户认证组件auth(登录用-依赖session,其他用)

    一.用户认证组件auth介绍 二.auth_user表添加用户信息 三.auth使用示例 四.auth封装的认证装饰器 一.用户认证组件auth介绍 解决的问题: 之前是把is_login=True放 ...

  4. 37行代码构建无状态组件通信工具-让恼人的Vuex和Redux滚蛋吧!

    状态管理的现状 很多前端开发者认为,Vuex和Redux是用来解决组件间状态通信问题的,所以大部分人仅仅是用于达到状态共享的目的.但是通常Redux是用于解决工程性问题的,用于分离业务与视图,让结构更 ...

  5. 无状态Web应用集成——《跟我学Shiro》

    http://www.tuicool.com/articles/iu2qEf 在一些环境中,可能需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时带上 ...

  6. 第二十章 无状态Web应用集成——《跟我学Shiro》

    目录贴:跟我学Shiro目录贴 在一些环境中,可能需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时带上相应的用户名进行登录.如一些REST风格的AP ...

  7. Shiro学习(20)无状态Web应用集成

    在一些环境中,可能需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时带上相应的用户名进行登录.如一些REST风格的API,如果不使用OAuth2协议, ...

  8. shiro实现无状态的会话,带源码分析

    转载请在页首明显处注明作者与出处 朱小杰      http://www.cnblogs.com/zhuxiaojie/p/7809767.html 一:说明 在网上都找不到相关的信息,还是翻了大半天 ...

  9. 关于Spring Security中无Session和无状态stateless

    Spring Security是J2EE领域使用最广泛的权限框架,支持HTTP BASIC, DIGEST, X509, LDAP, FORM-AUTHENTICATION, OPENID, CAS, ...

随机推荐

  1. mysql 案例~ 主从复制转化为级联复制

    一 需求 mysql 主从复制切换成级联复制二 核心思想 1 开启级联复制 2 确定postion点场景 A->B A-C 三 切换步骤  1 先确定好B为级联复制库  2 B添加log_upd ...

  2. jquery 学习(六) - 事件绑定

    HTML <!--绑定事件--> <div class="a1"> <button class="bt">按钮</bu ...

  3. pl/sql Devloper 如何查看表结构

    在命令行 敲  desc 表名:

  4. linux 查看用户上次修改密码的日期【转】

    1.找到以下文件: cat /etc/shadow 第三段字符就是最近一次密码修改的天数,此数字是距离1970年1月1日的天数.   2.用以下命令计算: date -u -d "1970- ...

  5. ubuntu数据库迁移

    环境:ubuntu16.04 简介:本教程演示如何从旧数据库服务器服转移到另一个新服务器. 场景:假设你有自己的云服务器安装了WordPress站点,你为了更多的内存和处理能力想升级到新的服务器. 操 ...

  6. 前端web服务器数据同步方案

    概述: 网站采用了web和mysql数据库分离的架构,前端有web1.web2.web3需要对他们进行上传文件同步 方案: 在web2的windows服务器上安装GoodSync软件,利用其双向同步特 ...

  7. vue 安装教程(自己安装过程及遇到的一些坑)

    1.安装node.js(http://www.runoob.com/nodejs/nodejs-install-setup.html) 2.基于node.js,利用淘宝npm镜像安装相关依赖 在cmd ...

  8. Java验证码

    下面这段代码可用于Jsp+Servle+JavaBean中做验证码: <%@ page contentType="image/jpeg" import="java. ...

  9. Java编码与乱码问题

    一.为什么要编码? 由于人类的语言太多,因而表示这些语言的符号太多,无法用计算机的一个基本的存储单元----byte来表示,因而必须要经过拆分或一些翻译工作,才能让计算机能理解. byte一个字节即8 ...

  10. poj3666 线性dp

    要把一个序列变成一个不严格的单调序列,求最小费用 /* 首先可以证明最优解序列中的所有值都能在原序列中找到 以不严格单增序列为例, a序列为原序列,b序列为升序排序后的序列 dp[i][j]表示处理到 ...