安全框架我们使用JWT(Json Web Token)和Spring Security
JWT官网:https://jwt.io/introduction
Spring Security官网:https://spring.io/projects/spring-security
Spring Security教程:https://www.w3cschool.cn/springsecurity/
第一步、添加pom依赖
在mango-admin的pom文件中添加Spring Security 和 JWT 依赖包
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.4</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
第二步、添加配置类
新建security包
登录认证过滤器
在包下添加登录认证过滤器JwtAuthenticationFilter 并继承 BasicAuthenticationFilter
重写doFilterInternal进行token校验
/**
* 登录认证过滤器
* @author Louis
* @date Jan 14, 2019
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
}
}
我们把验证逻辑放到SecurityUtils 的checkAuthentication方法中,checkAuthentication通过JwtTokenUtils的方法获取认证信息并保存到SpringSecurity上下文
JwtTokenUtils.java
/**
* JWT工具类
* @author Louis
* @date Jan 14, 2019
*/
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 authentication 用户
* @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 request
* @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;
}
}
SecurityUtils .java
/**
* Security相关操作
* @author Louis
* @date Jan 14, 2019
*/
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;
}
}
在config包下添加Spring Security配置类WebSecurityConfig.java
主要进行安全相关配置,如URL匹配策略、认证过滤、开启权限认证注解等
/**
* Spring Security配置
* @author Louis
* @date Jan 14, 2019
*/
@Configuration
@EnableWebSecurity // 开启Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,如:@PreAuthorize注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@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()
// web jars
.antMatchers("/webjars/**").permitAll()
// 查看SQL监控(druid)
.antMatchers("/druid/**").permitAll()
// 首页和登录页面
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
// swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
// 验证码
.antMatchers("/captcha.jpg**").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();
}
}
身份验证类
在Security包下新建JwtAuthenticationProvider类并继承DaoAuthenticationProvider
重写additionalAuthenticationChecks方法来进行密码匹配,我们这里没有使用默认的密码认证器(我们使用的盐salt来对密码加密,默认密码认证器没有加盐)
JwtAuthenticationProvider.java
/**
* 身份验证提供者
* @author Louis
* @date Jan 14, 2019
*/
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"));
}
}
}
认证信息查询
在security包下新建UserDetailsServiceImpl类实现UserDetailsService
重新其中的loadUserByUsername,查询用户的密码和权限信息封装到JwtUserDetails中
JwtUserDetails类需要实现UserDetails,是对认证信息的封装,包含用户名、密码、加密盐和权限信息
JwtUserDetails.java
/**
* 安全用户模型
* @author Louis
* @date Jan 14, 2019
*/
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;
}
}
UserDetailsServiceImpl.java
/**
* 用户登录认证信息查询
* @author Louis
* @date Jan 14, 2019
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.findByName(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
// 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
Set<String> permissions = sysUserService.findPermissions(user.getName());
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
}
}
权限分装类
在security包下新建GrantedAuthorityImpl类实现GrantedAuthority是对权限的封装,内部包含一个字符串类型的权限标识authority,
对应菜单表的perms字段的权限字符串,比如用户管理新增权限标志sys:user:add
/**
* 权限封装
* @author Louis
* @date Jan 14, 2019
*/
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;
}
}
添加权限注解
spring security提供了权限注解@PreAuthorize来保护后台接口免受非法访问
这里我们以字典模块为例,其他模块同理
SysDictController.java
/**
* 字典控制器
* @author Louis
* @date Jan 13, 2019
*/
@RestController
@RequestMapping("dict")
public class SysDictController {
@Autowired
private SysDictService sysDictService;
//用户必须同事拥有添加和修改权限才有权限
@PreAuthorize("hasAuthority('sys:dict:add') AND hasAuthority('sys:dict:edit')")
@PostMapping(value="/save")
public HttpResult save(@RequestBody SysDict record) {
return HttpResult.ok(sysDictService.save(record));
}
@PreAuthorize("hasAuthority('sys:dict:delete')")
@PostMapping(value="/delete")
public HttpResult delete(@RequestBody List<SysDict> records) {
return HttpResult.ok(sysDictService.delete(records));
}
@PreAuthorize("hasAuthority('sys:dict:view')")
@PostMapping(value="/findPage")
public HttpResult findPage(@RequestBody PageRequest pageRequest) {
return HttpResult.ok(sysDictService.findPage(pageRequest));
}
@PreAuthorize("hasAuthority('sys:dict:view')")
@GetMapping(value="/findByLable")
public HttpResult findByLable(@RequestParam String lable) {
return HttpResult.ok(sysDictService.findByLable(lable));
}
}
Swagger添加令牌参数
由于我们引入的spring security 接口受到保护,需要有合法的token才能正常访问,swagger本身默认并没有,因此需要修改一些之前SwaggerConfig配置类,修改后代码
@EnableSwagger2
@Configuration
public class SwaggerConfig {
@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();
parameters.add(parameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
.build().globalOperationParameters(parameters);
//return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder().build();
}
}
配置完启动项目,访问swagger就可以传参了
登录接口实现
在登录控制器中添加login接口,在其中验证验证码、用户名、密码信息,匹配成功后执行spring security的登录认证机制,登录成功后返回Token令牌凭证
/**
* 登录接口
*/
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
String username = loginBean.getAccount();
String password = loginBean.getPassword();
String captcha = loginBean.getCaptcha();
// 从session中获取之前保存的验证码跟前台传来的验证码进行匹配
Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(kaptcha == null){
return HttpResult.error("验证码已失效");
}
if(!captcha.equals(kaptcha)){
return HttpResult.error("验证码不正确");
}
// 用户信息
SysUser user = sysUserService.findByName(username);
// 账号不存在、密码错误
if (user == null) {
return HttpResult.error("账号不存在");
}
if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) {
return HttpResult.error("密码不正确");
}
// 账号锁定
if (user.getStatus() == 0) {
return HttpResult.error("账号已被锁定,请联系管理员");
}
// 系统登录认证
JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
return HttpResult.ok(token);
}
登录认证逻辑我们已经封装到了上面的SecurityUtils的login方法中,认证流程大体分为以下4步
1、将用户名密码的认证信息封装到JwtAuthenticatioToken对象
2、通过调用authenticationManager.authenticate(token)执行认证流程
3、通过SecurityContextHolder将认证信息保存到Security上下文
4、JwtTokenUtils.generateToken(authentication)生成token并返回
新建LoginBean.java
对登录认证信息简单封装
/**
* 登录接口封装对象
* @author Louis
* @date Oct 29, 2018
*/
public class LoginBean {
private String account;
private String password;
private String captcha;
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCaptcha() {
return captcha;
}
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
}
令牌自定义
在Security包下新建JwtAuthenticatioToken.java继承UsernamePasswordAuthenticationToken
对令牌信息简单封装,用来作为认证和授权的信息凭证,其中token信息有JWT生成
/**
* 自定义令牌对象
* @author Louis
* @date Jan 14, 2019
*/
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;
}
}
到此我们已经完成配置下面进行Swagger测试
先执行验证码生成接口获取验证码
然后执行登录接口,输入用户名和密码和刚生成的验证码,登录成功后返回token
在调用其他接口不输入token,直接403,说明我们安全校验生效
输入token在调用,可以看到执行成功
希望可以帮助大家,记得点赞哦