为了方便测试首先在swagger config中配置加入令牌项,配置token作为请求头部参数:
@Configuration
@EnableSwagger2
public class SwaggerConfig
{
/** 是否开启swagger */
@Value("true")
private boolean enabled;
/**
* 创建API
*/
@Bean
public Docket createRestApi() {
// 添加请求参数,我们这里把token作为请求头部参数传入后端
ParameterBuilder parameterBuilder = new ParameterBuilder();
List<Parameter> parameters = new ArrayList<Parameter>();
parameterBuilder.name("token").description("令牌")
.modelRef(new ModelRef("string")).parameterType("header").required(false).build(); //关键是这里,将token作为了请求头参数
parameters.add(parameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(enabled)
.select()
.apis(RequestHandlerSelectors.basePackage("com.english.english_vision"))
.paths(PathSelectors.any())
.build().globalOperationParameters(parameters);
}
private ApiInfo apiInfo() {
.....
}
}
这样写了以后运行会发现在请求时多了一行token:
在登陆成功后会返回token,用这个token去请求其他接口即可
新建securityconfig类
@Primary
//由于报There is more than one bean of 'UserDetailsService' 所以我在类上面加了@primary 不过不加也不影响运行
@Configuration
@EnableWebSecurity // 开启Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,如:@PreAuthorize注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//UserDetailsService 是rpingsecurity中自己的接口
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/user/login").permitAll()//这里的路径可以自己配置
.antMatchers("/security/save").permitAll()//注册
.antMatchers("/security/login").permitAll()//登录
// swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
// 服务监控
.antMatchers("/actuator/**").permitAll()
// 其他所有请求需要身份认证
.anyRequest().authenticated();
// 退出登录处理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
// token验证过滤器
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
spring security整合jwt的代码较多,目录如下:
@RestController
@RequestMapping("/security")
@Api(tags = {"安全登录接口"})
public class UserSecurityController {
@Autowired
private IUserSecurityService securityService;
@Autowired
private AuthenticationManager authenticationManager;
@ApiOperation(value = "注册或者修改用户")
@PostMapping(value = "/save")
public ResponseResult save(UserSecurity record) {
boolean save=false;
UserSecurity userSecurity = securityService.getById(record.getId());
if (record.getPassword() != null) {
String salt = PasswordUtils.getSalt();
if (userSecurity == null) {
// 新增用户
if (securityService.selectByName(record.getName()) != null) {
return ResponseResult.error("用户名已存在!");
}
String password = PasswordUtils.encode(record.getPassword(), salt);
record.setSalt(salt);
record.setPassword(password);
} else {
// 修改用户, 且修改了密码
if (!record.getPassword().equals(userSecurity.getPassword())) {
String password = PasswordUtils.encode(record.getPassword(), salt);
record.setSalt(salt);
record.setPassword(password);
}
}
save = securityService.save(record);
}
if (save) {
return ResponseResult.success(record);
} else {
return ResponseResult.error(ResponseEnum.ERROR);
}
}
@ApiOperation(value = "用户登录")
@PostMapping(value = "/login")
public ResponseResult login(@RequestBody LoginBean loginBean, HttpServletRequest request)
{
String username = loginBean.getName();
String password = loginBean.getPassword();
// 用户信息
UserSecurity user = securityService.selectByName(username);
// 账号不存在、密码错误
if (user == null) {
return ResponseResult.error("账号不存在");
}
if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) {
return ResponseResult.error("密码不正确");
}
// 系统登录认证
JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
return ResponseResult.success(token);
}
}
加密是:
String password = PasswordUtils.encode(record.getPassword(), salt);
验证是:
if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) {
}
加密时只会对明文密码进行加密然后与数据库进行匹配,不会对密文解密,这样更加安全。
public interface IUserSecurityService extends IService<UserSecurity> {
UserSecurity selectByName(String username);
Set<String> findPermissions(String userName);
}
@Service
public class UserSecurityServiceImpl extends ServiceImpl<UserSecurityMapper, UserSecurity> implements IUserSecurityService {
@Autowired
private UserSecurityMapper securityMapper;
@Override
public UserSecurity selectByName(String username) {
return securityMapper.selectByName(username);
}
@Override
public Set<String> findPermissions(String userName) {
return securityMapper.findPermissions(userName);
}
}
loginbean:
/**
* @Author
* @Description 登录接口封装对象
* @Date
**/
@Data
public class LoginBean {
private String name;
private String password;
// private String captcha;
}
用户pojo:
@NoArgsConstructor
@AllArgsConstructor
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_user_security")
@ApiModel("用户安全管理")
public class UserSecurity implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(name = "id", value = "用户id", dataType = "Integer")
@TableId(value = "id", type = IdType.AUTO)
private Integer id=null;
@ApiModelProperty(name = "name", value = "用户名", dataType = "String")
private String name;
@ApiModelProperty(name = "password", value = "用户密码", dataType = "String")
private String password;
@ApiModelProperty(name = "salt", value = "密码盐", dataType = "String")
private String salt;
@ApiModelProperty(name = "perm", value = "用户权限", dataType = "String")
private String perm;
}
mapper:
@Mapper
public interface UserSecurityMapper extends BaseMapper<UserSecurity> {
@Select("select * from t_user_security where name=#{username}")
UserSecurity selectByName(String username);
@Select("select perm from t_user_security where name=#{username}")
Set<String> findPermissions(String userName);
}
/**
* 密码工具类
*/
public class PasswordUtils {
/**
* 匹配密码
* @param salt 盐
* @param rawPass 明文
* @param encPass 密文
* @return
*/
public static boolean matches(String salt, String rawPass, String encPass) {
return new PasswordEncoder(salt).matches(encPass, rawPass);
}
/**
* 明文密码加密
* @param rawPass 明文
* @param salt
* @return
*/
public static String encode(String rawPass, String salt) {
return new PasswordEncoder(salt).encode(rawPass);
}
/**
* 获取加密盐
* @return
*/
public static String getSalt() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 20);
}
}
加密:
public class PasswordEncoder {
private final static String[] hexDigits = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d",
"e", "f" };
private final static String MD5 = "MD5";
private final static String SHA = "SHA";
private Object salt;
private String algorithm;
public PasswordEncoder(Object salt) {
this(salt, MD5);
}
public PasswordEncoder(Object salt, String algorithm) {
this.salt = salt;
this.algorithm = algorithm;
}
/**
* 密码加密
* @param rawPass
* @return
*/
public String encode(String rawPass) {
String result = null;
try {
System.out.println("加密"+rawPass);
MessageDigest md = MessageDigest.getInstance(algorithm);
// 加密后的字符串
result = byteArrayToHexString(md.digest(mergePasswordAndSalt(rawPass).getBytes("utf-8")));
} catch (Exception ex) {
}
return result;
}
/**
* 密码匹配验证
* @param encPass 密文
* @param rawPass 明文
* @return
*/
public boolean matches(String encPass, String rawPass) {
String pass1 = "" + encPass;
String pass2 = encode(rawPass);
System.out.println("密文"+encPass);
System.out.println(rawPass+"加密后"+pass2);
return pass1.equals(pass2);
}
private String mergePasswordAndSalt(String password) {
if (password == null) {
password = "";
}
if ((salt == null) || "".equals(salt)) {
return password;
} else {
return password + "{" + salt.toString() + "}";
}
}
/**
* 转换字节数组为16进制字串
*
* @param b
* 字节数组
* @return 16进制字串
*/
private String byteArrayToHexString(byte[] b) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++) {
resultSb.append(byteToHexString(b[i]));
}
return resultSb.toString();
}
/**
* 将字节转换为16进制
* @param b
* @return
*/
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n = 256 + n;
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}
package com.english.english_vision.config.springSecurity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* @Author
* @Description 身份验证提供者
* @Date 10.19
**/
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
setUserDetailsService(userDetailsService);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
String salt = ((JwtUserDetails) userDetails).getSalt();
// 覆写密码验证逻辑
if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
package com.english.english_vision.config.springSecurity;
import java.util.Collection;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtAuthenticatioToken(Object principal, Object credentials){
super(principal, credentials);
}
public JwtAuthenticatioToken(Object principal, Object credentials, String token){
super(principal, credentials);
this.token = token;
}
public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
super(principal, credentials, authorities);
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public static long getSerialversionuid() {
return serialVersionUID;
}
}
JwtAuthenticatioToken继承自UsernamePasswordAuthenticationToken,
UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验(Authentication),
然后生成的Authentication会被交由AuthenticationManager来进行管理,而AuthenticationManager管理一系列的AuthenticationProvider,每一个Provider都会通UserDetailsService和UserDetail来返回一个以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
//AuthenticationManager这个接口方法入参和返回值的类型都是Authentication。该接口的作用是对用户的未授信凭据进行认证,认证通过则返回授信状态的凭据,否则将抛出认证异常AuthenticationException。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
}
}
BasicAuthenticationFilter:当一个HTTP请求中包含一个名字为Authorization的头部,并且其值格式是Basic xxx时,该Filter会认为这是一个BASIC authorization头部,其中xxx部分应该是一个base64编码的{username}:{password}字符串。比如用户名/密码分别为 admin/secret, 则对应的该头部是 : Basic YWRtaW46c2VjcmV0 。
该过滤器会从 HTTP BASIC authorization头部解析出相应的用户名和密码然后调用AuthenticationManager进行认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。同时还会做其他一些处理,比如Remember Me相关处理等等。
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名称
*/
private static final String USERNAME = Claims.SUBJECT;
/**
* 创建时间
*/
private static final String CREATED = "created";
/**
* 权限列表
*/
private static final String AUTHORITIES = "authorities";
/**
* 密钥
*/
private static final String SECRET = "abcdefgh";
/**
* 有效期12小时
*/
private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;
/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
public static String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 根据请求令牌获取登录认证信息
* @param token 令牌
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取请求携带的令牌
String token = JwtTokenUtils.getToken(request);
if(token != null) {
// 请求令牌不能为空
if(SecurityUtils.getAuthentication() == null) {
// 上下文中Authentication为空
Claims claims = getClaimsFromToken(token);
if(claims == null) {
return null;
}
String username = claims.getSubject();
if(username == null) {
return null;
}
if(isTokenExpired(token)) {
return null;
}
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if (authors != null && authors instanceof List) {
for (Object object : (List) authors) {
authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username, null, authorities, token);
} else {
if(validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 验证令牌
* @param token
* @param username
* @return
*/
public static Boolean validateToken(String token, String username) {
String userName = getUsernameFromToken(token);
return (userName.equals(username) && !isTokenExpired(token));
}
/**
* 刷新令牌
* @param token
* @return
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 获取请求token
* @param request
* @return
*/
public static String getToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if(token == null) {
token = request.getHeader("token");
} else if(token.contains(tokenHead)){
token = token.substring(tokenHead.length());
}
if("".equals(token)) {
token = null;
}
return token;
}
}
public class SecurityUtils {
/**
* 系统登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程
Authentication authentication = authenticationManager.authenticate(token);
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
/**
* 获取令牌进行认证
* @param request
*/
public static void checkAuthentication(HttpServletRequest request) {
// 获取令牌并根据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* 获取当前用户名
* @return
*/
public static String getUsername() {
String username = null;
Authentication authentication = getAuthentication();
if(authentication != null) {
Object principal = authentication.getPrincipal();
if(principal != null && principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
}
return username;
}
/**
* 获取用户名
* @return
*/
public static String getUsername(Authentication authentication) {
String username = null;
if(authentication != null) {
Object principal = authentication.getPrincipal();
if(principal != null && principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
}
return username;
}
/**
* 获取当前登录信息
* @return
*/
public static Authentication getAuthentication() {
if(SecurityContextHolder.getContext() == null) {
return null;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication;
}
}
public class GrantedAuthorityImpl implements GrantedAuthority {
private static final long serialVersionUID = 1L;
private String authority;
public GrantedAuthorityImpl(String authority) {
this.authority = authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return this.authority;
}
}
public class JwtUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String salt;
private Collection<? extends GrantedAuthority> authorities;
JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.salt = salt;
this.authorities = authorities;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
public String getSalt() {
return salt;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
配置好以后先注册:
返回注册成功的信息:
然后测试登录:
结果:
可以看到返回的token,复制下来然后用这个token测试其他接口即可
首先在不加权限注解时测试这个接口:
@ApiOperation(value="管理员添加某个单词")
@PostMapping("admin/uploadword")
public ResponseResult uploadWord(@RequestParam("wordfile")MultipartFile file,Word word) throws IOException {
System.out.println("my CopyrightYear:"+new Myconfig().getCopyrightYear());
String Wordpath = uploadUtils.upload(new Myconfig().getWordPath(),file);
word.setImg(Wordpath);
if(wordService.insert(word)>0)
{
return ResponseResult.success(word);
}
else {
return ResponseResult.error(ResponseEnum.ERROR);
}
}
用刚刚生成的令牌进行测试:
可以看到添加成功:
现在在这个接口方法上加上权限注解:
@PreAuthorize(“hasAuthority(‘sys::manage’)”) ,重新运行:
由于刚刚登录的用户权限是sys::use,所以无法访问这个接口:
现在重新登录一个有sys::manage权限的用户,获得token,再次访问这个接口:
访问成功。
参考:https://blog.csdn.net/andy_zhang2007/article/details/84920910
https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1319904585363980289&__biz=MzUzMzQ2MDIyMA==#wechat_redirect