我们目前有一个系统,使用spring data jpa访问数据库,使用spring mvc提供rest接口给网站系统,同时使用shiro提供权限控制,目前需要对外部系统提供接口,需要满足以下情况:
* 若目前已经存在了这样的接口, 不再另外提供,同时权限部分得满足;
* 提供外部系统的接口权限使用token实现,即外部系统需要先获取到token,然后将token设置到cookie上,系统再根据token获取用户信息判断权限,token在一定时间内有效。
* 提供给外部系统的不能访问原系统的接口(不共用的接口);
首先,每一个@RequestMapping可以配置多个不同的请求路径,对于外部接口提供的接口,添加API后缀即可区分,添加拦截器实现对后缀是API接口的拦截(获取token,并自动登陆系统),在完成请求时通过封装将结果同一格式返回到前台,用户登录时,将token和用户对于的user信息保存到缓存中。
/**
* @类名 KickoutSessionControlFilter
* @描述 多个用户登录踢出及session过期处理 拦截器
* @作者 zhuxl
* @创建日期 2016年12月30日 下午2:47:33
*/
public class KickoutSessionControlFilter implements Filter {
private Logger logger=Logger.getLogger(KickoutSessionControlFilter.class);
private UserSessionRedisService userSessionRedisService;
private UserService userService;
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request=(HttpServletRequest)req;
String url=request.getRequestURI();
if(url.matches("^.{0,}/api/\\w{1,}/\\w{1,}API.{0,}$")){//是外部接口访问,从cookie中获取token并登陆
Cookie[] cookies=request.getCookies();
if(cookies!=null&&cookies.length>0){
for(Cookie cookie:cookies){
if("token".equalsIgnoreCase(cookie.getName())){
String token=cookie.getValue();
String userId=userSessionRedisService.get(SessionType.API, token, -1);
User user=userService.findById(userId);
if(user!=null){
Subject curUser=SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getId(), user.getPassword());
try{
curUser.login(usernamePasswordToken);
}catch(Exception e){
e.printStackTrace();
}
request.getSession(true).setAttribute("user", user);
}
filterChain.doFilter(req, res);
return;
}
}
}
filterChain.doFilter(req, res);
return;
}
//web、微信登录 用户未登录,通过,后面使用shiro进行权限(角色)拦截
Subject subject = SecurityUtils.getSubject();
HttpServletResponse response = (HttpServletResponse) res;
if (!subject.isAuthenticated() && !subject.isRemembered()) {
filterChain.doFilter(req, res);
return;
}
Session session = subject.getSession(false);
if(session==null){
filterChain.doFilter(req, res);
return;
}
User user=(User)session.getAttribute("user");
SessionType type=(SessionType)session.getAttribute("loginType");
if(user==null||type==null){
filterChain.doFilter(req, res);
return;
}
//获取用户session
String sessionId=null;
if(type==SessionType.WECHAT){
sessionId=userSessionRedisService.get(type,user.getOpenId(), -1);
}else if(type==SessionType.WEB){
sessionId=userSessionRedisService.get(type,user.getId(),1800000);
}
if(StrUtil.isBlank(sessionId)){
//用户session已经失效
logger.info("登录失效了!");
try{
subject.logout();
}catch(Exception ex){
}
response.setStatus(510);
Map maps=new HashMap();
maps.put("code", ErrorCode.NoLogin);
maps.put("msg", "用户登录已失效!!");
response.getOutputStream().write(ReturnFormat.retParam(maps).getBytes());
return;
}else if(session.getId().equals(sessionId)){
//用户已经登录,并且是同一个session,继续
logger.info("用户已登录系统!!");
filterChain.doFilter(req, res);
return;
}else if(type==SessionType.WECHAT){
//用户已经在其他地方登录了,对于微信,直接挤上去
try{
subject.logout();
}catch(Exception ex){
ex.printStackTrace();
}
logger.info("用户已经在其他地方登录了,登录类型为微信,将直接挤上去");
User newUser=userService.findByOpenId(user.getOpenId());
if(newUser!=null){//存在该openId
Subject curUser=SecurityUtils.getSubject();
Session newSession=curUser.getSession(true);
newSession.setAttribute("user", newUser);
newSession.setAttribute("loginType", SessionType.WECHAT);
newSession.setTimeout(1000*60*60*24);
userSessionRedisService.add(type,newUser.getOpenId(), newSession.getId().toString(),-1);
UsernamePasswordToken token = new UsernamePasswordToken(
newUser.getOpenId(), newUser.getPassword());
try{
curUser.login(token);
}catch(Exception e){
e.printStackTrace();
}
filterChain.doFilter(req, res);
}else{//不存在openID
response.setStatus(510);
Map maps=new HashMap();
maps.put("code", ErrorCode.NoLogin);
maps.put("msg", "用户未绑定账号!!");
response.getOutputStream().write(ReturnFormat.retParam(maps).getBytes());
}
return ;
}else if(type==SessionType.WEB){
//用户已经在其他地方登录了,对于web,直接退出
logger.info("用户已经在其他地方登录了,登录类型为:web,直接退出");
try{
subject.logout();
}catch(Exception ex){
ex.printStackTrace();
}
response.setStatus(510);
Map maps=new HashMap();
maps.put("code", ErrorCode.NoLogin);
maps.put("msg", "用户已经在其他地方登录了");
response.getOutputStream().write(ReturnFormat.retParam(maps).getBytes());
return ;
}
}
//初始化
public void init(FilterConfig config) throws ServletException {
WebApplicationContext wac =WebApplicationContextUtils.getRequiredWebApplicationContext(config.getServletContext());
userSessionRedisService=(UserSessionRedisService)wac.getBean("userSessionRedisServiceImpl");
userService=(UserService)wac.getBean("userServiceImpl");
}
}
/**
* @类名 ErrorCode
* @描述 错误编码
* @作者 zhuxl
* @创建日期 2016年12月30日 下午4:28:38
*/
public enum ErrorCode {
/**
* ErrorCode NoLogin 未登录系统,需要退出
*/
NoLogin,
/**
* ErrorCode Error 普通错误,具体错看msg
*/
Error,
/**
* ErrorCode NoPermission 无权限访问
*/
NoPermission,
/**
* ErrorCode LossParam 缺少参数
*/
LossParam,
/**
* ErrorCode HasEntity 已经存在(唯一性存在)
*/
HasEntity,
/**
* ErrorCode NoEntity 没有对应的实体
*/
NoEntity,
/**
* ErrorCode HasLogin 已经登录了系统
*/
HasLogin,
/**
* ErrorCode OK 成功
*/
OK
}
/**
* @类名 ApiMsg
* @描述 Api调用返回
* @作者 zhuxl
* @创建时间 2017-4-19下午02:21:33
*/
public class ApiMsg implements Serializable{
/**
* long serialVersionUID
*/
private static final long serialVersionUID = -5590788323134429419L;
/**
* int code 编码 -1 未登录,1 成功,0 其他错误
*/
private ErrorCode code;
/**
* String msg 信息
*/
private String msg;
/**
* Object result 返回结果
*/
private Object result;
public ErrorCode getCode() {
return code;
}
public void setCode(ErrorCode code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public ApiMsg(ErrorCode code, String msg, Object result) {
super();
this.code = code;
this.msg = msg;
this.result = result;
}
public ApiMsg(Object result) {
this.code=ErrorCode.OK;
this.msg="成功";
this.result = result;
}
}
/**
* @类名 ApiResponseBodyAdvice
* @描述 对于外部系统调用,将统一给出ApiMsg作为返回结果
* @作者 zhuxl
* @创建时间 2017-5-26下午05:18:39
*/
@ControllerAdvice
public class ApiResponseBodyAdvice implements ResponseBodyAdvice<Object>{
@Override
public boolean supports(MethodParameter returnType, Class extends HttpMessageConverter>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class extends HttpMessageConverter>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof ApiMsg){
return body;
}else{
String url=request.getURI().getPath();
if(url.matches("^.{0,}/api/\\w{1,}/\\w{1,}API.{0,}$")){
//对string类型做特殊处理
if(body instanceof String&& StringHttpMessageConverter.class.equals(selectedConverterType)){
return JSONObject.toJSONString(new ApiMsg(body));
}else{
return new ApiMsg(body);
}
}else{
return body;
}
}
}
}
/**
* @类名 GlobalExceptionHandler
* @描述 全局异常处理
* @作者 zhuxl
* @创建时间 2017-5-26下午02:25:15
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private Logger logger=Logger.getLogger(GlobalExceptionHandler.class);
/**
* handleException
* @描述: 处理PhyException异常
* @作者: zhuxl
* @创建时间: 2016-7-14下午07:45:16
* @param request
* @param response
* @param phyException
* @return
*/
@ExceptionHandler(PhyException.class)
public @ResponseBody ApiMsg handleException(PhyException phyException,HttpServletRequest request,HttpServletResponse response){
String url=request.getRequestURI();
if(url.matches("^.{0,}/api/\\w{1,}/\\w{1,}API.{0,}$")){
return new ApiMsg(phyException.getCode(),phyException.getCode()==ErrorCode.NoLogin?"ssid失效或者未登录":phyException.getMessage(),null);
}else{
response.setStatus(510);
return new ApiMsg(phyException.getCode(),phyException.getMessage(),null);
}
}
/**
* 用户未登录系统
* @return
*/
@ExceptionHandler(UnauthenticatedException.class)
public @ResponseBody ApiMsg handleUnauthenticatedException(HttpServletRequest request,HttpServletResponse response){
String url=request.getRequestURI();
if(url.matches("^.{0,}/api/\\w{1,}/\\w{1,}API.{0,}$")){
return new ApiMsg(ErrorCode.NoLogin,"token失效或者未登录",null);
}else{
Subject subject=SecurityUtils.getSubject();
if(subject==null||!subject.isAuthenticated()){
response.setStatus(510);
return new ApiMsg(ErrorCode.NoLogin,"用户未登录系统",null);
}else{
response.setStatus(510);
return new ApiMsg(ErrorCode.NoLogin,"用户已经登录系统",null);
}
}
}
/**用户无权限访问
* @return
*/
@ExceptionHandler(AuthorizationException.class)
public @ResponseBody ApiMsg handleAuthorizationException(HttpServletRequest request,HttpServletResponse response){
String url=request.getRequestURI();
if(url.matches("^.{0,}/api/\\w{1,}/\\w{1,}API.{0,}$")){
return new ApiMsg(ErrorCode.NoPermission,"无权限访问",null);
}else{
response.setStatus(510);
return new ApiMsg(ErrorCode.NoPermission,"无权限访问",null);
}
}
/**
* handleOtherException
* @描述 其他错误返回
* @作者 zhuxl
* @创建时间 2017年2月20日 下午5:45:50
* @return
*/
@ExceptionHandler(value={Exception.class,Error.class})
public @ResponseBody ApiMsg handleOtherException(Throwable throwable,HttpServletRequest request,HttpServletResponse response){
String url=request.getRequestURI();
logger.error("系统出错",throwable);
if(url.matches("^.{0,}/api/\\w{1,}/\\w{1,}API.{0,}$")){
return new ApiMsg(ErrorCode.Error,"系统出错了",null);
}else{
response.setStatus(510);
return new ApiMsg(ErrorCode.Error,"系统出错了",null);
}
}
}
/**
* @类名 ApiInterceptor
* @描述 对API调用的接口进行拦截,退出其登录状态(前台将无JSESSIONID)
* @作者 zhuxl
* @创建时间 2017-4-21上午10:40:37
*/
public class ApiInterceptor extends HandlerInterceptorAdapter{
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String url=request.getRequestURI();
if(url.matches("^.{0,}/api/\\w{1,}/\\w{1,}API.{0,}$")){
org.apache.shiro.subject.Subject curUser=SecurityUtils.getSubject();
if(curUser.isAuthenticated()||curUser.isRemembered()){
try{
curUser.logout();
}catch (Exception e) {
e.printStackTrace();
}
}
/*HttpSession session=request.getSession(false);
if(session!=null){
session.invalidate();
}*/
}
}
}
/**
* loginApi
* @描述 外部接口登录,将返回一个token,token(32位)一天有效,过期后将无法使用,需要重新获取,重复调用将重复生成
* @作者 zhuxl
* @创建时间 2017-4-19下午03:21:45
* @param nameAndPwd
* @return
*/
@RequestMapping(value = {"loginAPI"}, method = RequestMethod.POST)
public String loginApi(@Valid @RequestBody LoginNameAndPwd nameAndPwd,BindingResult result){
if(result.hasFieldErrors()){
throw new PhyException(ErrorCode.Error,result.getFieldError().getDefaultMessage());
}
User user2 = userService.findByLoginName(nameAndPwd.getLoginName());
if (user2 == null) {
throw new PhyException(ErrorCode.Error,"当前登录用户不存在");
}
if (user2.getPassword().equals(CryptUtil.encrypt(nameAndPwd.getTextPwd(), user2.getSalt()))) {
String token=UUID.randomUUID().toString().replace("-", "");
userSessionRedisService.add(SessionType.API,token, user2.getId(),86400000L);
logger.info(new LogModel(true, "user api login system success,loginName:"+user2.getLoginName()));
return token;
} else {
throw new PhyException(ErrorCode.Error,"登录名或者密码不正确");
}
}
/**
* findMyStation
* @描述 查找我的监测站(包括状态)
* @作者 zhuxl
* @创建时间 2017年2月6日 下午1:51:04
* @param session
* @return
*/
@RequiresUser
@RequestMapping(value={"findMyStation","findMyStationAPI"},method=RequestMethod.GET)
public List findMyStation(HttpSession session){
User user=(User)session.getAttribute("user");
if(user==null){
throw new PhyException(ErrorCode.NoLogin,"未登录系统");
}
Role role=user.getRole();
List stations=new ArrayList();
List stationTags=new ArrayList();
if(role.getRoleName().equals("admin")){
stations=stationService.findAll();
}else{
stations=stationService.findByArea(user.getArea().getId());
}
//此处省略
}
<context:component-scan base-package="com.phy.smartcity.eps.*.openapi,com.phy.smartcity.eps.filter">
context:component-scan>
<bean id="jacksonMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
bean>
<mvc:interceptors>
<bean class="com.phy.smartcity.eps.filter.ApiInterceptor">bean>
mvc:interceptors>
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
<property name="validationMessageSource" ref="messageSource" />
bean>
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:ValidationMessages_zh_CNvalue>
<value>classpath:org/hibernate/validator/ValidationMessages_zh_CNvalue>
list>
property>
<property name="useCodeAsDefaultMessage" value="false" />
<property name="defaultEncoding" value="UTF-8" />
<property name="cacheSeconds" value="60" />
bean>
<bean
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonMessageConverter" />
list>
property>
bean>
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8" />
<property name="maxUploadSize" value="10485760" />
bean>
<mvc:annotation-driven validator="validator" />
<aop:config proxy-target-class="true">aop:config>
<bean
class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
bean>