项目开发中,后台系统尤为重要,最近就想搞一套能拿来直接使用的后台管理系统。
框架:
Shiro,Shiro作为安全框架,实现登录、登出、身份验证、授权、会话管理。
JWT,Json Web Token负责访问接口时的验证。
Vue,前端框架,因为本身不是前端开发,所以Clone了某位高人在github上的模版。写的非常好,简单易用。github.com/PanJiaChen/…
前端代码就是照葫芦画瓢,不过很多地方也google,还是很麻烦的,这里不多赘述。下面整理一下后端Shiro+JWT部分代码逻辑。
Shiro
@Configuration
public class ShiroConfig {
/*-------------------------------------------
| 哈 哈 |
============================================*/
/**
* 验证码验证filter
*
* @return
*/
@Bean(name = "captchaValidate")
public CaptchaValidateFilter captchaValidate() {
return new CaptchaValidateFilter();
}
/**
* jwt-filter
*
* @return
*/
@Bean(name = "jwt")
public JwtFilter jwtFilter() {
return new JwtFilter();
}
/**
* 登录时账户密码验证
*
* @return
*/
@Bean
public GeneralCredentialsMatcher generalCredentialsMatcher() {
return new GeneralCredentialsMatcher();
}
/**
* 账户验证,权限验证
*
* @return
*/
@Bean
public UserRealm myRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(generalCredentialsMatcher());
return userRealm;
}
@Bean(name = "shiroFilterChainDefinition")
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
definition.addPathDefinition("/api/user/login", "captchaValidate");
definition.addPathDefinition("/api/**", "jwt");
return definition;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
manager.setSessionManager(defaultWebSessionManager());
manager.setCacheManager(redisCacheManager());
return manager;
}
@Bean
public RedisManager redisManager() {
return new RedisManager();
}
@Bean
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
return defaultWebSessionManager;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
- captchaValidate()自定义filter负责处理验证码,在login接口的时候会触发。
- jwtFilter()继承了shiro的BasicHttpAuthenticationFilter类,重写了isAccessAllowed与onAccessDenied方法。这里这么做是解决跨域的时候,前端会发送OPTIONS探测方法,确认服务端允许跨域。这里简单处理一下让这种请求通过不被拦截,并验证如果不是OPTIONS请求,时候包含jwt的header信息
- generalCredentialsMatcher()负责处理login验证,查询数据库验证用户名密码。
- myRealm()自定义Realm继承shiro抽象类AuthorizingRealm,重写doGetAuthenticationInfo方法创建并保存用户信息,重写doGetAuthorizationInfo方法查询并保存用户角色权限信息。
- shiroFilterChainDefinition()配置shiro的filter调用链,在调用login接口时会触发验证码验证的filter,而其他所有接口都会触发jwt的filter。
- securityManager()配置DefaultWebSecurityManager,redis来保存session会话信息。
- Redis配置,开启shiro注解配置。
登录
@PostMapping(value = "/user/login")
public ResponseEntity<ApiResponse<LoginResponse>> login(@RequestBody @Validated LoginRequest loginRequest) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginRequest.getUsername(), loginRequest.getPassword());
token.setRememberMe(true);
subject.login(token);
LoginUser user = subject.getPrincipals().oneByType(LoginUser.class);
String sessionId = subject.getSession().getId().toString();
String jwtToken = JwtTokenUtils.createToken(TokenParam.builder()
.key(JwtTokenUtils.SESSION_KEY)
.value(sessionId)
.build());
LoginResponse loginResponse = LoginResponse.builder()
.name(user.getUserName())
.token(jwtToken)
.build();
log.info("login success");
return super.getApiResponseResponseEntity(loginResponse);
}
login接口没做拦截,首先拿到当前线程的Subject实例,用接口参数封装UsernamePasswordToken对象,调用subject.login(token);调用成功之后获取登录的成功后的sessionId作为jwt的一部分,再把数据封装后返回到前端。此时session保存在redis中。
关于会有SecurityUtils.getSubject().getPrincipal()为null的问题。是因为SecurityUtils.getSubject获取的是当前线程保存在ThreadLocal中的Subject对象。而如果是前后端分离的项目的话,每次请求都不会是之前的Thread了,那么就会出现null的情况。
如果是在其他方法上配置了注解验证的话,类似@RequiresRoles("admin"),那么注解的验证会更早于其他的shiro-filter配置,注解验证时还会调用SecurityUtils.getSubject()也会出现同样的null的问题。
为了解决SecurityUtils.getSubject().getPrincipal()为null,又添加了Spring的Interceptor,这样做是为了能在注解方法之前通过token中的sessionid去redis中查找shiro登录时配置的缓存。
public class ShiroSubjectInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String authorization = request.getHeader("Authorization");
String sessionId = JwtTokenUtils.getPayloadMapValue(authorization, JwtTokenUtils.SESSION_KEY);
if(StringUtils.isBlank(sessionId)) {
ApiResponse apiResponse = new ApiResponse();
apiResponse.setSubCode(ApiResponseStatusCodeEnum.TOKEN_EXPIRED.getSubCode());
apiResponse.setSubMessage(ApiResponseStatusCodeEnum.TOKEN_EXPIRED.getMessage());
response.getWriter().write(GsonUtils.getGsonWithOutConfig().toJson(apiResponse));
response.flushBuffer();
return false;
}
Subject subject = new Subject.Builder().sessionId(sessionId).buildSubject();
ThreadContext.bind(subject);
return true;
}
}
Subject subject = new Subject.Builder().sessionId(sessionId).buildSubject();从Redis取出缓存。 ThreadContext.bind(subject);绑定到当前线程的ThreadLocal中。 这样就解决了上面的问题。
vue
前端的话,大概几个功能,左侧菜单栏根据后端权限来动态加载,一些基本的权限控制,角色控制,一些增删改查。后续还会完善登录日志,操作日志。
页面
github:github.com/libinjimubo…