传统的web应用,使用jsp作为前端展示的情况下,大家习惯用的手段都是用户登录后,将用户的信息放到tomcat的session中保存,返回前端时,cookies中有个jsessionId。后续的各种操作都会用到这个jsessionId从tomcat的session列表中,读取用户的信息,再根据用户的信息做各种数据的处理。
这种处理方式在单web应用中问题是不大的。但是自从分布式、微服务等理念流行之后,各个服务组件之间的session共享问题就有点头疼了,毕竟每个服务或每个节点都是独立的tomcat,跨域等问题都让人无可奈何。
这时候能采用的手段就是tomcat自身的session复制,或者spring集成redis提出的session共享,但是解决起来都是比较恶心人的,感觉都不是很好用,并且对于架构方面的可扩展性都不是很好。
这时候token机制的优势就出来了,其中比较有代表性的就是jwt(Json Web Token),签发的token中具备用户的基本信息(可以是你认为有用的信息,如用户id,用户名,角色等),只要所有的服务或节点都采用同样的token处理机制,token验证机制就能识别用户的身份。
用户登录时,后端判断登录成功后,将有用的信息封装到token中,并且放到相应头(也有人喜欢放到相应体)中,根据项目实际情况来做。
前端拿到接收到登录响应后,从响应头中提取到token,接下来每次访问API,都将token放到请求头(也有人喜欢放到请求参数)中,并且每次接收到响应后,比较服务器返回的响应头token和本地是否一致(服务器有可能刷新token),不一致则覆盖本地token
后端接收到请求时,先验证token是否合法,不合法直接打回。再验证是否过期,过期则替换token(也可能要求再次登录),将原token放到黑名单,防止令牌泄漏。之后再去做相应的业务处理。
用户退出登录后,将原token放到黑名单,防止令牌泄漏。
前后端分离的架构下,比较痛苦的就是API如何鉴权,也就是哪些接口能让那些人访问。比如用户管理系统中,不是所有的用户都有权限去新增一个用户,或删除一个用户。但是所有的功能都由API实现,这时候要自己去业务逻辑里根据用户角色去判定吗?不需要的,这样太累。spring-security或帮你做到这一步,只需要你在接口上标注下,哪些角色允许调用就好了。
<!-- JWT依赖 -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>5.14</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- security支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- fastjson支持 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.40</version>
</dependency>
<!-- redis支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
package com.zyu.boot.demo.security.entity;
/**
* JWT+SpringSecurity验证API角色枚举
*
* @author zyu
*
*/
public enum Role {
//管理员
admin("admin"),
//普通用户
user("user");
//角色名称
String role;
Role(String role){
this.role = role;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return this.role;
}
}
package com.zyu.boot.demo.security.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Jwt中载荷包含的用户信息
* 同时为了和Spring-Security集成,实现了UserDetails(security要求实现)接口
*
* @author zyu
*
*/
public class JwtUser implements UserDetails {
private static final long serialVersionUID = 4394410605122716469L;
private String uid;// 用户ID
private String username;// 用户名
private List<Role> roles;// 角色
private String password;// 密码
public JwtUser() {
}
public JwtUser(String uid, String username, List<Role> roles) {
super();
this.uid = uid;
this.username = username;
this.roles = roles;
}
public String getUid() {
return uid;
}
public JwtUser setUid(String uid) {
this.uid = uid;
return this;
}
public String getUsername() {
return username;
}
public JwtUser setUsername(String username) {
this.username = username;
return this;
}
public List<Role> getRole() {
return roles;
}
public JwtUser setRole(List<Role> roles) {
this.roles = roles;
return this;
}
public JwtUser setPassword(String password) {
this.password = password;
return this;
}
// 权限信息,直接使用role中的信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
for (Role role : roles) {
auths.add(new SimpleGrantedAuthority("ROLE_" + role.toString()));
}
return auths;
}
@Override
public String getPassword() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
package com.zyu.boot.demo.utils.token;
import com.zyu.boot.demo.security.entity.JwtUser;
import com.zyu.boot.demo.security.entity.Role;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import java.util.*;
/**
* JWT相关工具类
*/
public class JwtTokenUtils {
/**
* 请求头中token的头
*/
public static final String TOKEN_HEADER = "token";
/**
* token的前缀
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 密钥key
*/
private static final String SECRET = "zyufocus0123456789zyufocus0123456789";
/**
* JWT的发行人
*/
private static final String ISS = "zyu";
/**
* 自定义信息中用户角色
*/
private static final String ROLE_CLAIMS = "role";
/**
* 自定义信息中用户ID
*/
private static final String UID_CLAIMS = "uid";
/**
* 自定义信息中用户名
*/
private static final String UNAME_CLAIMS = "uname";
/**
* 过期时间是3600秒,既是60min
*/
public static final long EXPIRATION = 3600L * 1000;
/**
* 选择了记住我之后的过期时间为1小时
*/
public static final long EXPIRATION_REMEMBER = 3600L * 1000;
/**
* 创建token
* @param details 用户登录信息
* @param isRememberMe 是否记住我
* @return
*/
public static String createToken(JwtUser details, boolean isRememberMe) {
// 如果选择记住我,则token的过期时间为
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, details.getAuthorities()); // 角色名字
map.put(UID_CLAIMS, details.getUid()); // 用户ID
map.put(UNAME_CLAIMS, details.getUsername()); // 用户名
return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET) // 加密算法
.setClaims(map) // 自定义信息
.setIssuer(ISS) // jwt发行人
.setSubject(details.getUsername()) // jwt面向的用户
.setIssuedAt(new Date()) // jwt发行时间
.setExpiration(new Date(System.currentTimeMillis() + expiration)) // key过期时间
.compact();
}
/**
* 获取用户信息
*
* @param token
* @return
*/
@SuppressWarnings("unchecked")
public static JwtUser getUserDetail(String token) {
Claims claims = getTokenBody(token);
JwtUser user = null;
if (claims != null && claims.size() > 0) {
user = new JwtUser();
if (claims.get(ROLE_CLAIMS) != null) {
ArrayList<Role> roles = new ArrayList<>();
ArrayList<LinkedHashMap<String, String>> list = (ArrayList<LinkedHashMap<String, String>>) claims
.get(ROLE_CLAIMS);
for (LinkedHashMap<String, String> l : list) {
roles.add(Role.valueOf(l.get("authority").replace("ROLE_", "")));
}
user.setRole(roles);
}
if (claims.get(UID_CLAIMS) != null) {
user.setUid((String) (claims.get(UID_CLAIMS)));
}
if (claims.get(UNAME_CLAIMS) != null) {
user.setUsername((String) (claims.get(UNAME_CLAIMS)));
}
}
return user;
}
/**
* 获取Token有效期ms
*
* @param token
* @return
*/
public static long getExpireTime(String token) {
Claims claims = getTokenBody(token);
return claims.getExpiration().getTime() - claims.getIssuedAt().getTime();
}
/**
* 从token获取用户信息
*
* @param token
* @return
*/
public static String getUsername(String token) {
return getTokenBody(token).getSubject();
}
/**
* 从token中获取用户角色
*
* @param token
* @return
*/
public static Set<String> getUserRole(String token) {
List<GrantedAuthority> userAuthorities = (List<GrantedAuthority>) getTokenBody(token).get(ROLE_CLAIMS);
return AuthorityUtils.authorityListToSet(userAuthorities);
}
/**
* 从token中获取用户ID
*
* @param token
* @return
*/
public static String getUserID(String token) {
return (String) getTokenBody(token).get(UID_CLAIMS);
}
/**
* 是否已过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
return getTokenBody(token).getExpiration().before(new Date());
}
/**
* 刷新token
*
* @param token 原token
* @param always 强制刷新,true有效
* @return
*/
public static String refreshToken(String token, boolean always) {
boolean canRefresh = canRefresh(token);
String news = null;
if (canRefresh == true || always == true) {
Claims claims = getTokenBody(token);
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, claims.get(ROLE_CLAIMS));
map.put(UID_CLAIMS, claims.get(UID_CLAIMS));
map.put(UNAME_CLAIMS, claims.get(UNAME_CLAIMS));
news = Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET) // 加密算法
.setClaims(map) // 自定义信息
.setIssuer(ISS) // jwt发行人
.setSubject((String) claims.get(UNAME_CLAIMS)) // jwt面向的用户
.setIssuedAt(new Date()) // jwt发行时间
.setExpiration(new Date(System.currentTimeMillis()
+ (claims.getExpiration().getTime() - claims.getIssuedAt().getTime()))) // key过期时间
.compact();
} else {
news = token;
}
return news;
}
/**
* 是否可刷新
*
* @param token
* @return
*/
public static boolean canRefresh(String token) {
Claims claims = getTokenBody(token);
long create = claims.getIssuedAt().getTime();
long expire = claims.getExpiration().getTime();
// 有效期不足一半,可刷新
return new Date().getTime() - create > (expire - create) / 2;
}
private static Claims getTokenBody(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
/**
* 验证token
*
* @param token
* @return
*/
public static boolean validateToken(String token) {
return (isExpiration(token) == false);
}
}
package com.zyu.boot.demo.utils.item;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
/**
* 统一的信息返回实体
*/
public class RespEntity implements Serializable {
private static final long serialVersionUID = -4164135841577282605L;
@ApiModelProperty(value = "错误码", example = "0", dataType = "Integer")
private Integer error_code = 0;
@ApiModelProperty(value = "错误信息", example = "操作成功", dataType = "String")
private String error_message = "操作成功";
@ApiModelProperty(value = "结果", example = "{}", dataType = "json")
private Object result;
public Integer getError_code() {
return error_code;
}
public void setError_code(Integer error_code) {
this.error_code = error_code;
}
public String getError_message() {
return error_message;
}
public void setError_message(String error_message) {
this.error_message = error_message;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public RespEntity(Integer error_code, String error_message, Object result) {
super();
this.error_code = error_code;
this.error_message = error_message;
this.result = result;
}
public RespEntity(Object result) {
this.result = result;
}
}
#redis配置
redis:
database: 0
host: 172.17.0.2
port: 6379
password:
jedis:
pool:
max-active: 8 #最大连接数
max-wait: 6000 #最大阻塞等待时间
max-idle: 8 #最大空闲连接
min-idle: 0 #最小空闲连接
timeout: 500
package com.zyu.boot.demo.security.config;
import com.zyu.boot.demo.security.filter.JwtAuthenticationTokenFilter;
import com.zyu.boot.demo.security.handler.JWTAuthenticationEntryPoint;
import com.zyu.boot.demo.security.handler.MyAccessDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 用来实现token验证
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// 权限不足处理器
@Bean
public MyAccessDeniedHandler myAccessDeniedHandler() {
return new MyAccessDeniedHandler();
};
// 认证失败处理器
@Bean
public AuthenticationEntryPoint getJWTAuthenticationEntryPoint() {
return new JWTAuthenticationEntryPoint();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 询问请求允许访问
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许对于网站静态资源的无授权访问
.antMatchers(HttpMethod.GET, "/", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/**/*.jpg",
"/**/*.png", "/**/*.woff2","/**/*.eot", "/**/*.ttf", "/**/*.gif")
.permitAll()
// 设置允许访问的资源(SwaggerAPI)
.antMatchers("/v2/api-docs", "/swagger-resources", "/swagger-resources/**", "/configuration/ui",
"/configuration/security", "/swagger-ui.html/**", "/webjars/**", "/doc.html",
"/v2/api-docs-ext")
.permitAll()
// 对于获取token的rest api要允许匿名访问
.antMatchers("/login/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 不在响应头中添加禁用缓存标记
http.headers().cacheControl().disable();
// 配置允许加载iframe
http.headers().frameOptions().disable();
// 添加JWT filter
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// http.formLogin().failureHandler(getMyAuthenctiationFailureHandler());
http
// 权限不足处理
.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler()).and()
// 认证失败处理
.exceptionHandling().authenticationEntryPoint(getJWTAuthenticationEntryPoint());
}
}
package com.zyu.boot.demo.security.handler;
import com.alibaba.fastjson.JSON;
import com.zyu.boot.demo.utils.item.RespEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义权限不足处理类
*
* @author zyu
*
*/
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
// 返回json形式的错误信息
//httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(JSON.toJSONString(new RespEntity(403, e.getMessage(), null)));
httpServletResponse.getWriter().flush();
}
}
package com.zyu.boot.demo.security.handler;
import com.alibaba.fastjson.JSON;
import com.zyu.boot.demo.utils.item.RespEntity;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
/**
* 自定义认证失败的处理器
*/
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8609737054427605012L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new RespEntity(401, e.getMessage(), null)));
}
}
package com.zyu.boot.demo.security.filter;
import com.alibaba.fastjson.JSON;
import com.zyu.boot.demo.security.entity.JwtUser;
import com.zyu.boot.demo.utils.item.RespEntity;
import com.zyu.boot.demo.utils.redis.RedisUtils;
import com.zyu.boot.demo.utils.token.JwtTokenUtils;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @description: token过滤器,用来验证token的有效性
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisUtils redisUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
if (token != null && token.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
token = token.substring(JwtTokenUtils.TOKEN_PREFIX.length());
} else {
filterChain.doFilter(request, response);
return;
}
try {
// 查询该令牌是否在黑名单中,存在返回true
boolean invalid = redisUtils.hasKey("tokenBlackList:" + token);
if (invalid == false && JwtTokenUtils.isExpiration(token) == false) {
if (JwtTokenUtils.canRefresh(token)) {
// 将原token放入黑名单
redisUtils.set("tokenBlackList:" + token, 1, JwtTokenUtils.getExpireTime(token) / 1000);
// 刷新令牌
token = JwtTokenUtils.refreshToken(token, false);
}
JwtUser user = JwtTokenUtils.getUserDetail(token);
if (user != null) {
// 制作身份认证标识,存入security域中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
user.getUsername(), null, user.getAuthorities());
authentication.setDetails(new WebAuthenticationDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
// 将已认证的用户信息,存入request域
request.setAttribute("currentUser", user);
}
// 存入request域
request.setAttribute(JwtTokenUtils.TOKEN_HEADER, token);
} else {
throw new Exception("验证失败");
}
} catch (Exception e) {
String msg = null;
if (e instanceof SignatureException) {
msg = "无效令牌";
} else {
msg = "认证失败";
}
response.setStatus(HttpStatus.UNAUTHORIZED.value());
// 允许跨域
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
response.setHeader("Access-Control-Allow-Headers",
"token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
// 允许前端拿到的header
response.setHeader("Access-Control-Expose-Headers",
"token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new RespEntity(401, msg, null)));
return;
}
filterChain.doFilter(request, response);
}
}
package com.zyu.boot.demo.utils.respbodyadvice;
import com.zyu.boot.demo.utils.token.JwtTokenUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 用于ResponseBody注解的controller方法处理的响应中自定义响应头(添加token信息)
*
* @author zyu
*
*/
@ControllerAdvice
public class HeaderModifierAdvice 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) {
ServletServerHttpRequest ssReq = (ServletServerHttpRequest) request;
ServletServerHttpResponse ssResp = (ServletServerHttpResponse) response;
if (ssReq == null || ssResp == null || ssReq.getServletRequest() == null
|| ssResp.getServletResponse() == null) {
return body;
}
// 响应头添加token
HttpServletRequest req = ssReq.getServletRequest();
HttpServletResponse resp = ssResp.getServletResponse();
String tokenHeader = JwtTokenUtils.TOKEN_HEADER;
//响应头中不包含token,且request域中有token值
if (resp.containsHeader(tokenHeader) == false && req.getAttribute(tokenHeader) != null) {
Object token = req.getAttribute(tokenHeader);
if (token != null) {
resp.setHeader(tokenHeader, JwtTokenUtils.TOKEN_PREFIX + (String)token);
}
}
return body;
}
}
@PostMapping("/userLogin")
public RespEntity userLogin(@RequestParam("account") String account, @RequestParam("password")String password, HttpServletRequest req){
User user = loginService.userLogin(account, password);
if(user != null){
ArrayList<Role> roles = new ArrayList<>();
roles.add(Role.valueOf(user.getRole()));
JwtUser jwtUser = new JwtUser().setRole(roles).setUid(user.getUserid());
// token信息保存在request域,随后保存在响应头
String token = JwtTokenUtils.createToken(jwtUser, false);
req.setAttribute("currentUser", user);
req.setAttribute(JwtTokenUtils.TOKEN_HEADER, token);
return new RespEntity(user);
}
return new RespEntity(-1,"账户名或密码错误",null);
}
/**
* 创建用户
* @param user
* @return
*/
@PreAuthorize("hasRole('admin')")
@ApiOperation(value = "创建用户", notes = "创建用户")
@PostMapping("/create")
public User create(@RequestBody User user){
return userService.createUser(user);
}
别忘了用git保存下哦