记录一下:企业级项目完成该功能的具体过程和原理 + 踩过的那些坑
首先是一些配置相关的准备工作
<properties>
<java.version>8java.version>
<fastjson.version>1.2.62fastjson.version>
<sdk.version>4.5.0sdk.version>
<mybatis.version>2.1.3mybatis.version>
<mybatis.plus.version>3.5.1mybatis.plus.version>
<mysql.version>8.0.25mysql.version>
<dynamic.version>3.3.2dynamic.version>
<druid.version>1.1.23druid.version>
<shiro.version>3.2.1shiro.version>
<jwt.version>0.9.1jwt.version>
<hutool.version>5.6.6hutool.version>
<generator.version>3.2.0generator.version>
properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>dynamic-datasource-spring-boot-starterartifactId>
<version>${dynamic.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis.plus.version}version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusartifactId>
<version>${mybatis.plus.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baidu.aipgroupId>
<artifactId>java-sdkartifactId>
<version>${sdk.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>${fastjson.version}version>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
dependency>
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redis-spring-boot-starterartifactId>
<version>${shiro.version}version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>${hutool.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>${jwt.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>${generator.version}version>
dependency>
<dependency>
<groupId>javax.validationgroupId>
<artifactId>validation-apiartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
server:
port: 8001
#这里配置访问项目的http根路径,即:http://localhost:8001/picture 类似于@RequestMapping("picture")
servlet:
context-path: /picture
mybatis:
mapper-locations: classpath*:com/example/**/mapping/*.xml
spring:
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
#热部署生效
devtools:
restart:
enabled: true
#设置重启的目录,添加那个目录的文件需要restart
additional-paths: src/main/java
additional-exclude: WEB-INF/**
#设置单个文件最大请求10MB,最多一次请求10个文件
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
#"关闭缓存, 即时刷新"
freemarker:
cache: false
#spring.thymeleaf.cache=true 如果开启此处会导致每次输入删除都会自动刷新哪怕你没保存
datasource:
druid:
#配置初始化大小/最小/最大
initial-size: 1
max-active: 100
min-idle: 1
#获取连接等待超时时间
max-wait: 60000
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
#Oracle需要打开注释
#validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
filter:
stat:
log-slow-sql: true
merge-sql: false
slow-sql-millis: 1000
wall:
config:
multi-statement-allow: true
dynamic:
primary: second
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源
datasource:
first:
url: jdbc:mysql://127.0.0.2:3306/guns?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&nullCatalogMeansCurrent=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
second:
url: jdbc:mysql://127.0.0.1:3306/guns?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&nullCatalogMeansCurrent=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
main:
allow-bean-definition-overriding: true
#jwt 相关信息
markerhub:
jwt:
# 加密秘钥
secret: f4e2e52034348f86b67cde581c09eb5
# token有效时间 单位秒
expire: 360
#jwt在header中的key
header: Authorization
#shiro redis的配置
shiro-redis:
enabled: true
redis-manager:
host: 127.0.0.1:6379
这里的配置:markerhub是我们自定义的参数,后面的jwtutils会读取这三个参数信息,当生成token信息的时候就会通过这个秘钥加密,并设置token的过去时间
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* jwt工具类
* @author
*/
@ConfigurationProperties(prefix = "markerhub.jwt")
@Component
public class JwtUtils {
private Logger logger = LoggerFactory.getLogger(getClass());
private String secret;
private long expire;
private String header;
/**
* 生成jwt token
*/
public String generateToken(long userId) {
Date nowDate = new Date();
//过期时间
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("type", "JWT")
.setSubject(userId + "")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
logger.debug("validate is token error ", e);
return null;
}
}
/**
* token是否过期
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public long getExpire() {
return expire;
}
public void setExpire(long expire) {
this.expire = expire;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
}
@ConfigurationProperties(prefix = “markerhub.jwt”) 该注解会在项目启动时自动读取yml配置文件中markerhub.jwt开头的配置信息,并赋值到对应的属性上面
准备工作完成,接下来开始配置shiro的登录认证
Security Manager是shiro的安全管理中心,可以看到每个用户都带有一个subject对象,subject就是shiro安全管理中心用来验证的对象,是平台与shiro交互的接口,我们只需要考虑将什么样的信息传入到subject对象中让shiro去验证。
虽然Security Manager可以自行去验证subject对象信息的是否正确,但是具体的验证逻辑和验证方式是需要我们自己定义的。所以我们还需要重写shiro的验证规则对象,即realm,realm对象就是一个权限管理规则,即满足什么样的要求视为验证通过
总体流程:首先请求从前端发送过来时,会被Filter拦截(这里可以在shiroConfig中可以配置不去拦截的请求),拦截请求后需要获取header中的token信息,并校验token是否存在,如果token不存在直接响应请求失败信息,如果token存在则执行登录(这里并不是真正的登录,只是去验证了token信息),执行登录会调用我们重写的realm验证规则来验证token信息是否有效,验证通过则访问接口,否则返回错误
/**
* tocken
*/
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String jwt){
this.token = jwt;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
重写三个方法:
supports:该方法是为了使realm对象支持我们自定义的token对象
doGetAuthorizationInfo: 该方法是验证用户是否拥有某种数据操作的权限,只有当触发检测用户权限时才会调用此方法,具体验证方法自定义
doGetAuthenticationInfo:用来进行身份认证,每次访问接口时会通过该方法进行验证token信息是否有效(当我们的filter校验token存在后,就会执行shrio的登录方法,登录方法就会通过realm中这个规则进行校验token)
具体代码可参考:
/**
* 验证机制
* @author
*/
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
JwtUtils jwtUtils;
@Autowired
MUserService mUserService;
/**
* 判断是否支持JwtToken
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 功能: 获取用户权限信息,包括角色以及权限。只有当触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("============用户授权==============");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
/*获取当前的用户,已经登录后可以使用在任意的地方获取用户的信息*/
String userId = (String) SecurityUtils.getSubject().getPrincipal();
return null;
}
/**
* 功能: 用来进行身份认证,也就是说验证用户输入的账号和密码是否正确,获取身份验证信息,错误抛出异常
* 处理登录认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
@DS("second")
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("============用户验证==============");
JwtToken jwtToken = (JwtToken) token;
Claims claim = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal());
if (claim == null || jwtUtils.isTokenExpired(claim.getExpiration())){
throw new ExpiredCredentialsException("token已过期,请重新登录");
}
String userid = claim.getSubject();
if (userid == null){
throw new UnknownAccountException("账户不存在");
//throw new LockedAccountException("账户被锁定");
}
//通过subject的id获取用户
MUser byId = mUserService.getById(userid);
if(byId == null){
throw new UnknownAccountException("账户不存在");
}
//将用户信息返回给shiro
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(byId, token.getCredentials(), getName());
return info;
}
}
这里验证登录失败后可以直接抛出异常,因为我这里写了全局异常处理,全局异常处理会将这个错误信息响应给前端,前端会处理
@RestControllerAdvice//所有RestController 类抛异常都会被这个异常类捕获
@Log4j
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)//返回错误状态码
@ExceptionHandler(value = RuntimeException.class)//RuntimeException异常处理方法
public Result handler(RuntimeException e){
return Result.isfail(e.getMessage());
}
@ResponseStatus(HttpStatus.UNAUTHORIZED)//返回错误状态码(该异常表示没有权限)
@ExceptionHandler(value = ShiroException.class)//Shiro的异常处理方法
public Result handler(ShiroException e){
return Result.isfail(e.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)//IllegalArgumentException异常处理方法
public Result handler(IllegalArgumentException e){
return Result.isfail(500,e.getMessage());
}
@ExceptionHandler(Exception.class)//Exception异常处理方法
public Result handler(Exception e){
log.error(e.getMessage());
return Result.isfail(500,e.getMessage());
}
}
3.自定义拦截器,重写shiro拦截器的实现,具体实现代码:
@Component
public class JwtFilter extends AuthenticatingFilter {
/**
* 重写shiro的生成token的方法 (利用jwt生成自定义的token) 此后shiro会通过这个token进行login
* @return
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//将请求转换为HttpServletRequest
HttpServletRequest request = (HttpServletRequest) servletRequest;
//获取请求头中的jwt信息
String jwt = request.getHeader("Authorization");
if (StringUtils.isEmpty(jwt)) {
return null;
}
return new JwtToken(jwt);
}
/**
* 拦截校验token
* @param servletRequest
* @param servletResponse
* @param mappedValue
* @return
*/
@Override
public boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) {
//Always return true if the request’s method is OPTIONS
if (servletRequest instanceof HttpServletRequest) {
if (((HttpServletRequest) servletRequest).getMethod().toUpperCase().equals("OPTIONS")) {
return true;
}
}
return false;
}
/**
* 拒绝访问的请求会进入这个方法处理
* isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//将请求转换为HttpServletRequest
HttpServletRequest request = (HttpServletRequest) servletRequest;
String jwt = request.getHeader("Authorization");
//获取请求头中的jwt信息
if (StringUtils.isEmpty(jwt)) {
// HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
// httpResponse.getWriter().print(JSONUtil.toJsonStr(Result.isfail("token已过期,请重新登录")));
return onLoginFailure(null,new ExpiredCredentialsException("请登录后访问该资源"),servletRequest,servletResponse);
//jwt为空后 不需要拦截 通过后接口会通过注解进行异常处理
//return true;
}
//执行登录
return executeLogin(servletRequest, servletResponse);
}
/**
* 重写登录失败的方法 失败后自定义失败响应信息
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
Throwable throwable = e.getCause() == null ? e : e.getCause();
Result isfail = Result.isfail(throwable.getMessage());
String jsonfail = JSONUtil.toJsonStr(isfail);
HttpServletResponse servletResponse = (HttpServletResponse) response;
try {
//解决传输中文乱码问题
servletResponse.setHeader("Content-Type","text/plain;charset=UTF-8");
servletResponse.getWriter().print(jsonfail);
} catch (IOException ioException) {
ioException.printStackTrace();
}
return false;
}
/**
* 处理过滤器跨域问题
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
//跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request,response);
}
}
重点记录一下拦截过程:
访问白名单不会被拦截,其他路径会被这个拦截器拦截,首先请求到达这个拦截器会调用isAccessAllowed()方法,进行校验,校验通过返回true后直接访问接口,返回false就会进入到onAccessDenied()方法,表示改请求被拦截,在这个方法中会进行token的简单校验,校验不通过返回false,并将登录失败的信息响应给前端,校验通过会执行登录操作executeLogin(并不是登录账号,只是去通过realm校验token)通过我们重写的realm进行进一步的校验,检验通过则访问接口
@Configuration
public class ShiroConfig {
@Autowired(required = false)
RedisSessionDAO redisSessionDAO;
@Autowired(required = false)
RedisCacheManager redisCacheManager;
// 自动装配filter交给shiro框架管理这是一个坑,要改为new对象的形式给shiro框架管理,具体问题下面介绍
// @Autowired
// JwtFilter jwtFilter;
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
//因为我本地没有启动redis,所以这里注释掉,shiro就不结合使用redis管理token信息了
// defaultWebSessionManager.setSessionDAO(redisSessionDAO);
return defaultWebSessionManager;
}
/**
* 这个是核心,重写securityManager管理中心,将我们自定义的realm校验规则传入securityManager,
* 下面还有一些其他的重写配置也一并交给securityManager
* @param realms
* @param sessionManager
* @return
*/
@Bean
public SessionsSecurityManager securityManager(AccountRealm realms, SessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realms);
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
/**
* 开启注解模式
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* 配置shiro生命周期
* @return
*/
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
LifecycleBeanPostProcessor processor = new LifecycleBeanPostProcessor();
return processor;
}
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
/**
* 定义过滤器
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
// 申请一个默认的过滤器链
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
Map<String,String> filterMap = new LinkedHashMap<>(0);
// filterMap.put("/**","authc");
chainDefinition.addPathDefinitions(filterMap);
return chainDefinition;
}
/**
* 过滤器工厂业务
* @param securityManager shiro中的安全管理
* @param shiroFilterChainDefinition
* @return
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition){
/*shiro过滤器bean对象*/
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 需要添加的过滤规则
Map<String, Filter> filters = new HashMap<>();
//下面这行代码是一个坑,不能使用自动装配的filter,要通过new的方式,具体问题下面介绍
//filters.put("jwt", jwtFilter);
filters.put("jwt", new JwtFilter());
shiroFilter.setFilters(filters);
Map<String,String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
/**
* anon:无需认证
* authc:必须认证
* user:如果使用rememberMe可直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到资源权限才可以访问
*/
//配置不会被拦截的链接
filterMap.put("/login","anon");
filterMap.put("/regist","anon");
filterMap.put("/logout","logout");
//添加一个jwt过滤器到过滤器链中
filterMap.put("/**","jwt");
shiroFilter.setFilterChainDefinitionMap(filterMap);
//需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
shiroFilter.setLoginUrl("/login");
return shiroFilter;
}
说明一下这个config配置类
因为我这个shiro框架是集成了redis的,所以在配置时要配置sessionManager(),sessionManager使用redis存储,所以自动装配了redisSessionDAO和redisCacheManager,这两个都是shiro的api,
> 重要的几个配置:
一,securityManager(),这个方法是将我们重写的一些配置交给shiro的安全管理中心,例如我们自定义的realm验证规则,和重写的sessionManager,cacheManager
二,shiroFilterFactoryBean(),在写这个配置之前要配置shiroFilterChainDefinition(),其实这两个配置共同完成了一件事情,就是指定我们自定一个Filter拦截器,并且配置一些访问白名单等(即哪些请求是可以没有token,不需要登录就能访问的)
重点说明(“/**”,“jwt”)这个配置是其他所有访问路径都会通过KEY为“jwt”的验证机制进行验证,这个“jwt”的验证机制就是我们在上面添加的Filter,所以这两个KEY要一致,这里你也可以配置多个filter,可以实现不同的请求会走不同的拦截器进行拦截
其他的配置基本就是一些shiro的标准配置,声明周期、开启注解模式,这些都是什么东西可以在网上得到答案
这里记录一下上面的几个坑:
刚开始我使用的是自动装配JwtFilter拦截器,然后在配置中将这个拦截器交给securityManager安全管理中心去管理,结果导致我配置的访问白名单不生效,所有的请求都会被拦截,就很坑,所以这里必须通过new对象的形式去管理
还有一个坑就是添加的访问请求白名单一定要在jwt过滤之前put到map中,因为这个map是一个LinkedHashMap,有序map,否则白名单不生效
上面的流程基本就是shiro验证的过程
接下来就是shiro验证通过访问到接口之后的操作了:(这里我只用了登录和注册,进行测试的,实际情况中这两个请求要配置白名单,不需要验证token)
@DS("second")
@DSTransactional
@PostMapping(path = "/login")
public Result login(@Validated @RequestBody MUser mUser) {
QueryWrapper<MUser> wrapper = new QueryWrapper<>();
wrapper.eq("username", mUser.getUsername());
MUser one = mUserService.getOne(wrapper);
if (one == null){
return Result.isfail("用户名密码不正确");
}
// Assert.notNull(one, "用户名密码不正确");
if (!one.getPassword().equals(SecureUtil.md5(mUser.getPassword()))) {// md5加密
return Result.isfail("用户名密码不正确");
}
String jwt = jwtUtils.generateToken(one.getId());// 生成jwt
// 返回用户信息
return Result.isSucc(MapUtil.builder()
.put("id", one.getId())
.put("username", one.getUsername())
.put("avatar", one.getAvatar())
.put("email", one.getEmail())
.put("jwt", jwt)// 生成的jwt
.map());
}
登录成功后会通过用户信息生成token信息(这里我把用户id存储到subject中生成jwt信息,可以根据自己需要将自己特定的信息存储,下次访问接口时realm也要验证这信息),然后将jwt信息响应到前端页面
到此后端所要做的事情基本完成了,要实现每次请求都携带这个jwt信息就要通过前端进行完成了
我们需要知道vue项目启动之后入口页面都是App.vue页面,这个页面可以当做其他页面的模板页面
<template>
<div id="app">
<router-view/>
div>
template>
这里的
只有这个标签是没有办法做到跳转的,还需要在路由router的index.js文件中配置一个redirect重定向路径
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import demoFile1 from '@/business/demo1/demoFile1'
import loginPage from '@/business/login/index'
Vue.use(Router)
//获取原型对象上的push函数
const originalPush = Router.prototype.push
//修改原型对象中的push方法
Router.prototype.push = function push(location) {
return originalPush.call(this, location)
// .catch(err => err)
}
export const routes = [
{ path: '/', redirect:'/login'},
{ path: '/login' , component: loginPage, name: 'login'},
{ path: '/demo1', name: 'demo1', component: demoFile1 },
{ path: '/HelloWorld', name: 'HelloWorld', component: HelloWorld },
]
export default new Router({
routes
})
{ path: ‘/’, redirect:‘/login’} 这个配置就是做重定向的关键,当访问http://localhost:8080这个路径时,就会触发路由的重定向到http://localhost:8080/#/login这个路径
其他三个就是配置各自页面的路由地址
import Vue from 'vue';
import App from './App';
import VueRouter from 'vue-router';
import router from './router';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios';
//引入axios
Vue.prototype.$axios = axios;
Vue.use(VueRouter);
Vue.use(ElementUI,{ size: 'small', zIndex: 3000 });
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: ' ',
render: h => h(App)
})
引入之后,接下来就是整合axios,axios作为ajax请求的封装,我们要再次封装一下request等请求,因为我们需要在每次请求时拦截请求并将登录之后的token信息存入到请求头header中
所以我们需要创建两个工具类:auth.js、request.js
auth.js 是一个针对token的get,set,remove等方法集合,我这里使用sessionStorage存储token信息
request.js 就是对axios的request和response的一个封装
具体代码:
auth.js
const TokenKey = 'loginToken'
export function getToken(){
return sessionStorage.getItem(TokenKey)
}
export function setToken(token) {
return sessionStorage.setItem(TokenKey, token)
}
export function removeToken() {
return sessionStorage.removeItem(TokenKey)
}
request.js
import axios from "axios";
import { getToken } from "./auth";
import { Message, MessageBox } from 'element-ui'
//创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 60000 // 请求超时时间
});
service.interceptors.request.use(config => {
// Do something before request is sent
config.headers['Content-Type'] = 'application/json;charset=UTF-8';
config.headers['Access-Control-Expose-Headers'] = 'Authorization';
config.headers['Authorization'] = getToken()
return config;
}, error => {
//当请求出错时我们需要处理的逻辑可以写在这里
console.log(error); // for debug
//报错后抛出异常 我们需要捕获异常再处理
Promise.reject(error);
});
service.interceptors.response.use(
response => {
const res = response.data;
if (res.code !== 200) {
Message({
message: res.message,
type: 'error',
duration: 3 * 1000
})
// 401:未登录;
if (res.code === 401) {
MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload()// 为了重新实例化vue-router对象 避免bug
})
})
}
// return Promise.reject('error');
return Promise.reject(res);
// return res
} else {
return response.data;
}
},
error => {
console.log('err' + error.message); // for debug
Message({
message: error.message,
type: 'error',
duration: 3 * 1000
})
return Promise.reject(error);
}
)
export default service;
这个文件我们需要引入axios、auth.js、element-ui(需要提示失败等信息)
需要注意的是:
创建axios实例的两个参数,baseURL(请求host路径)、timeout(请求超时时间)
重点是baseURL,这个值为process.env.BASE_API,如果你的项目启动的时候是npm run dev的话,BASE_API使用的就是dev环境配置的BASE_API
这个配置在config目录下的dev.env.js文件中,prodEnv是打包时使用的配置信息,npm run build时会将prod.env.js文件的BASE_API地址打包进去,我们本地运行时会采用下面的这个配置地址
http://localhost:8001/picture 配置后面的这个/picture地址是因为,后端项目的配置文件中配置了访问更路径
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:8001/picture"',
})
host地址配置完成,接下来就是配置请求和响应的封装处理
service.interceptors.request 意思就是每次去拦截request请求
config.headers[‘Content-Type’] = ‘application/json;charset=UTF-8’;
config.headers[‘Access-Control-Expose-Headers’] = ‘Authorization’;
config.headers[‘Authorization’] = getToken()
然后向request请求的header中添加这几个参数信息,将token信息添加到请求中
service.interceptors.response 每次拦截响应信息,如果返回code 不成功则给予响应的提示信息
如果 return Promise.reject(res); 则代表抛出异常,这时你的请求代码中需要捕获异常,再异常中处理返回信息,并提示错误
如果 return res; 则代表将响应信息返回给你的请求代码,你可以自行处理这个响应信息
两者的区别就是 一个需要你捕获异常,另一个不需要捕获异常,下面代码会有演示
接下来就是在login页面 完成登录了
import request from '@/utils/request'
export function Login(user) {
return request({
url: '/login',
method: 'post',
data: user
})
}
然后来到登录页面,index.vue页面
import { register, Login } from '@/api/Login';
export default{
//...这里其他的组件就不写了
methods: {
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {//表单信息校验通过
this.loading = true;
Login(this.loginForm).then(res => {
this.loading = false;
if (res.code == 200) {
this.$message({
message: '登录成功',
type: 'success'
});
sessionStorage.setItem('username', this.loginForm.username);
sessionStorage.setItem('password', this.loginForm.password);
sessionStorage.setItem('loginToken', res.data.jwt);
this.$router.push({ path: '/demo1' })
} else {
removeToken
this.$message.error(res.message);
}
}).catch(() => {
removeToken
this.loading = false
})
} else {
console.log('参数验证不合法!');
return false
}
})
}
}
}
登录请求成功后就会将token信息存入到sessionStorage,下次请求就会将这个token信息携带到请求头中了
先记录到这吧。。。。。