Spring Boot 集成教程


在教程 [spring boot rest 接口集成 spring security(1) - 最简配置] 里介绍了最简集成spring security的过程,本文将继续介绍spring boot项目中集成spring security以及配置jwt的过程。

如果不了解jwt,可以参考5分钟搞懂:JWT(Json Web Token)

项目内容

本文将通过创建一个实际的spring boot项目来演示spring security及jwt的配置过程,项目主要内容:

  • 集成spring security;
  • 配置jwt;
  • 加载用户信息;
  • 实现几个接口,配置访问权限;
  • 最后通过Postman测试接口;

要求

  • JDK1.8或更新版本
  • Eclipse开发环境

如没有开发环境,可参考前面章节 [spring boot 开发环境搭建(Eclipse)]。

项目创建

创建spring boot项目

打开Eclipse,创建spring boot的spring starter project项目,选择菜单:File > New > Project ...,弹出对话框,选择:Spring Boot > Spring Starter Project,在配置依赖时,勾选web, security,完成项目创建。

项目依赖

要使用jwt,引入jwt jar包

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

项目配置

application.properties配置

## 服务器端口,如果不配置默认是8080端口
server.port=8096 ## jwt配置
# 签名密钥
jwt.secret=my_secret_2019
# jwt有效期(秒)
jwt.expiration=1800

代码实现

项目目录结构如下图,我们添加了几个类,下面将详细介绍。

spring security的配置:SecurityConfig.java

这是spring security的java配置类,几个主要的配置:

  • 用户信息加载配置
  • 权限不足处理配置
  • 权限配置
  • jwt过滤器配置
  • 其他如密码加密,CORS等配置

@Configuration
@EnableWebSecurity // 添加security过滤器
@EnableGlobalMethodSecurity(prePostEnabled = true) // 可以在controller方法上配置权限
public class SecurityConfig extends WebSecurityConfigurerAdapter{ // 加载用户信息
@Autowired
private UserDetailsService myUserDetailsService; // 权限不足错误信息处理,包含认证错误与鉴权错误处理
@Autowired
private JwtAuthError myAuthErrorHandler; // 密码明文加密方式配置
@Bean
public PasswordEncoder myEncoder() {
return new BCryptPasswordEncoder();
} // jwt校验过滤器,从http头部Authorization字段读取token并校验
@Bean
public JwtAuthFilter myAuthFilter() throws Exception {
return new JwtAuthFilter();
} // 获取AuthenticationManager(认证管理器),可以在其他地方使用
@Bean(name="authenticationManagerBean")
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
} // 认证用户时用户信息加载配置,注入myUserDetailsService
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
} // 配置http,包含权限配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 由于使用的是JWT,我们这里不需要csrf
.csrf().disable() // 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 设置myUnauthorizedHandler处理认证失败、鉴权失败
.exceptionHandling().authenticationEntryPoint(myAuthErrorHandler).accessDeniedHandler(myAuthErrorHandler).and() // 设置权限
.authorizeRequests() // 需要登录
.antMatchers("/hello/hello1").authenticated() // 需要角色权限
.antMatchers("/hello/hello2").hasRole("ADMIN") // 除上面外的所有请求全部放开
.anyRequest().permitAll(); // 添加JWT过滤器,JWT过滤器在用户名密码认证过滤器之前
http.addFilterBefore(myAuthFilter(), UsernamePasswordAuthenticationFilter.class); // 禁用缓存
// http.headers().cacheControl();
} // 配置跨源访问(CORS)
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}

用户信息及用户信息服务:AuthUser.java,AuthUserService.java

加载用户信息,需要用户信息类及用户信息服务类。AuthUser继承spring的UserDetails,必须重写UserDetails的一些标准接口。注意与实体类User区别。


public class AuthUser implements UserDetails { private static final long serialVersionUID = -2336372258701871345L; //用户实体类
private User user; public AuthUser(User user) {
this.setUser(user);
} public static Collection<? extends GrantedAuthority> getAuthoritiesByRole(String role) {
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); List<String> roles = Arrays.asList(role.split(","));
if (roles.contains("user")) {
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
}
if (roles.contains("admin")) {
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
} return authorities;
} // 提供权限信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { return getAuthoritiesByRole(getUser().getRole());
} // 提供账号名称
@Override
public String getUsername() {
return getUser().getMobile();
} // 提供密码
@Override
public String getPassword() {
return getUser().getPassword();
} // 账号是否没过期,过期的用户无法认证
@Override
public boolean isAccountNonExpired() {
return true;
} // 账号是否没锁住,锁住的用户无法认证
@Override
public boolean isAccountNonLocked() {
return true;
} // 密码是否没过期,密码过期的用户无法认证
@Override
public boolean isCredentialsNonExpired() {
return true;
} // 用户是否使能,未使能的用户无法认证
@Override
public boolean isEnabled() {
return true;
} public User getUser() {
return user;
} public void setUser(User user) {
this.user = user;
} }

AuthUserService继承UserDetailsService,重写了加载用户信息接口:

@Service
public class AuthUserService implements UserDetailsService { // 加载用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 此处应从数据库加载用户信息,为简便起见,直接创建一个用户
// password的值:$2a$10$EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG,
// 是明文123456使用BCryptPasswordEncoder加密的值
User user = new User(1l, "abc1", username, "$2a$10$EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG", "user");
AuthUser authUser = new AuthUser(user); return (UserDetails) authUser;
}
}

认证失败、鉴权失败处理:JwtAuthError.java

当认证失败,系统会抛出认证失败异常,可以配置我们自己的认证失败处理类,同样鉴权失败也可以配置我们自己的失败处理类。

JwtAuthError继承AuthenticationEntryPoint(认证失败接口)、AccessDeniedHandler(鉴权失败接口),重写了这2个接口类的失败处理方法,其实JwtAuthError可以分为2个类,我们合二为一了。

@Component
public class JwtAuthError implements AuthenticationEntryPoint, AccessDeniedHandler { @SuppressWarnings("unused")
private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthError.class); // 认证失败处理,返回401 json数据
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"status\":401,\"message\":\"Unauthorized or invalid token\"}"); } // 鉴权失败处理,返回403 json数据
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"status\":403,\"message\":\"Forbidden\"}");
}
}

JWT过滤器

JWT过滤器每次请求应该只执行一次,所以继承OncePerRequestFilter,JWT过滤器的主要行为:

  • 对于每次请求,从http头部Authorization字段中读取jwt
  • 尝试解密jwt,如果正常解出,说明是合法用户
  • 如果是合法用户,设置认证信息,认证通过

@Component
public class JwtAuthFilter extends OncePerRequestFilter { private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthFilter.class); @Autowired
private JwtUtil jwtUtil; private String tokenHeader="Authorization"; private String tokenPrefix="Bearer"; @Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException { // 从http头部读取jwt
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenPrefix)) { final String authToken = authHeader.substring(tokenPrefix.length() + 1); // The part after "Bearer "
String username = null, role = null; // 从jwt中解出账号与角色信息
try {
username = jwtUtil.getUsernameFromToken(authToken);
role = jwtUtil.getClaimFromToken(authToken, "role", String.class);
} catch (Exception e) {
log.debug("异常详情", e);
log.info("无效token");
} // 如果jwt正确解出账号信息,说明是合法用户,设置认证信息,认证通过
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username, null, AuthUser.getAuthoritiesByRole(role)); // 把请求的信息设置到UsernamePasswordAuthenticationToken details对象里面,包括发请求的ip等
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 设置认证信息
SecurityContextHolder.getContext().setAuthentication(auth); }
} // 调用下一个过滤器
chain.doFilter(request, response);
}
}

User实体类(model层)

User实体类对应于数据库中的User表(我们简化了,没有连数据库)


public class User {
private Long id; private String nickname; private String mobile; private String password; private String role; public User(Long id, String nickname, String mobile, String password, String role) {
this.id = id;
this.nickname = nickname;
this.mobile = mobile;
this.password = password;
this.role = role;
} public User() {
super();
}
}

LoginRequest类(model层)

登录请求类,这个类将会接受并校验用户登录时输入的账号密码,关于输入校验,可以参考 [spring boot输入数据校验(validation)]


public class LoginRequest { @SuppressWarnings("unused")
private static final org.slf4j.Logger log = LoggerFactory.getLogger(LoginRequest.class); @NotNull(message="账号必须填")
@Pattern(regexp = "^[1]([3][0-9]{1}|59|58|88|89)[0-9]{8}$", message="账号请输入11位手机号") // 手机号
private String account; @NotNull(message="密码必须填")
@Size(min=6, max=16, message="密码6~16位")
private String password; private boolean rememberMe; 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 boolean isRememberMe() {
return rememberMe;
}
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
} }

AuthController类(控制层)

AuthController类实现了2个REST API:

  • login - 用户提供账号密码,如果密码正确,返回token,否则返回账号或密码错误提示;
  • refresh 输入一个合法的旧token,返回新token

@RestController
@RequestMapping("/auth")
public class AuthController { @Autowired
private AuthService authService; /**
* login
* @param authRequest
* @param bindingResult
* @return ResponseEntity<Result>
*/
@RequestMapping(value = "/login", method = RequestMethod.POST, produces="application/json")
public ResponseEntity<Result> login(@Valid @RequestBody LoginRequest authRequest, BindingResult bindingResult) throws AuthenticationException{ if(bindingResult.hasErrors()) {
Result res = MiscUtil.getValidateError(bindingResult);
return new ResponseEntity<Result>(res, HttpStatus.UNPROCESSABLE_ENTITY);
} final String token = authService.login(authRequest.getAccount(), authRequest.getPassword()); // Return the token
Result res = new Result(200, "ok");
res.putData("token", token);
return ResponseEntity.ok(res);
} /**
* refresh
* @param request
* @return ResponseEntity<Result>
*/
@RequestMapping(value = "/refresh", method = RequestMethod.GET, produces="application/json")
public ResponseEntity<Result> refresh(HttpServletRequest request, @RequestParam String token) throws AuthenticationException{ Result res = new Result(200, "ok"); String refreshedToken = authService.refresh(token); if(refreshedToken == null) {
res.setStatus(400);
res.setMessage("无效token");
return new ResponseEntity<Result>(res, HttpStatus.BAD_REQUEST);
} res.putData("token", token);
return ResponseEntity.ok(res);
} }

HelloController类(控制层)

实现了3个REST API:

  • hello1
  • hello2
  • hello3

用于测试权限配置


@RestController
@RequestMapping("/hello")
public class HelloController { @RequestMapping(value="/hello1", method=RequestMethod.GET)
public String hello1() { return "Hello1!";
} @RequestMapping(value="/hello2", method=RequestMethod.GET)
public String hello2() { return "Hello2!";
} @RequestMapping(value="/hello3", method=RequestMethod.GET)
public String hello3() { return "Hello3!";
}
}

AuthService接口与AuthServiceImpl实现类(服务层)

AuthService提供对AuthController的服务

AuthService.java

public interface AuthService {
User register(User userToAdd);
String login(String username, String password);
String refresh(String oldToken);
}

AuthServiceImpl.java


@Service
public class AuthServiceImpl implements AuthService { private static final org.slf4j.Logger log = LoggerFactory.getLogger(AuthServiceImpl.class); private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private JwtUtil jwtUtil; @Autowired
public AuthServiceImpl(
AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
} @Override
public User register(User userToAdd) {
// TODO: 保存user到数据库
return null;
} @Override
public String login(String username, String password) {
// 认证用户,认证失败抛出异常,由JwtAuthError的commence类返回401
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // 如果认证通过,返回jwt
final AuthUser userDetails = (AuthUser) userDetailsService.loadUserByUsername(username);
final String token = jwtUtil.generateToken(userDetails.getUser());
return token;
} @Override
public String refresh(String oldToken) {
String newToken = null; try {
newToken = jwtUtil.refreshToken(oldToken);
} catch (Exception e) {
log.debug("异常详情", e);
log.info("无效token");
}
return newToken;
}
}

其他

剩下的一些类

  • Result.java 结果封装类
  • MiscUtil.java 辅助类
  • JwtUtil.java jwt处理类,加密解密等操作

运行

Eclipse左侧,在项目根目录上点击鼠标右键弹出菜单,选择:run as -> spring boot app 运行程序。 打开Postman访问接口,运行结果如下:

访问/hello/hello1接口,需要登录访问,没有带上token,返回401

登录获取token

再次访问需要登录访问的/hello/hello1接口,带上token,可以看到访问成功

访问需要admin权限的/hello/hello2接口,虽然带上token,但权限不足,可以看到返回403

总结

完整代码

spring boot rest 接口集成 spring security(2) - JWT配置的更多相关文章

  1. spring boot rest 接口集成 spring security(1) - 最简配置

    Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...

  2. 【Spring】关于Boot应用中集成Spring Security你必须了解的那些事

    Spring Security Spring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架.除了常规的Authentication和A ...

  3. 关于Boot应用中集成Spring Security你必须了解的那些事

    Spring Security Spring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架.除了常规的Authentication和A ...

  4. spring boot / cloud (三) 集成springfox-swagger2构建在线API文档

    spring boot / cloud (三) 集成springfox-swagger2构建在线API文档 前言 不能同步更新API文档会有什么问题? 理想情况下,为所开发的服务编写接口文档,能提高与 ...

  5. Spring Boot HikariCP 一 ——集成多数据源

    其实这里介绍的东西主要是参考的另外一篇文章,数据库读写分离的. 参考文章就把链接贴出来,里面有那位的代码,简单明了https://gitee.com/comven/dynamic-datasource ...

  6. 【ELK】4.spring boot 2.X集成ES spring-data-ES 进行CRUD操作 完整版+kibana管理ES的index操作

    spring boot 2.X集成ES 进行CRUD操作  完整版 内容包括: ============================================================ ...

  7. (转)Spring Boot 2 (八):Spring Boot 集成 Memcached

    http://www.ityouknow.com/springboot/2018/09/01/spring-boot-memcached.html Memcached 介绍 Memcached 是一个 ...

  8. Spring Boot系列——如何集成Log4j2

    上篇<Spring Boot系列--日志配置>介绍了Spring Boot如何进行日志配置,日志系统用的是Spring Boot默认的LogBack. 事实上,除了使用默认的LogBack ...

  9. Spring Boot 2 (八):Spring Boot 集成 Memcached

    Spring Boot 2 (八):Spring Boot 集成 Memcached 一.Memcached 介绍 Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数 ...

随机推荐

  1. navcat工具常用快捷键

     navcat工具常用快捷键 ctrl + n: 打开新查询窗口 ctrl + shit + r: 只运行选中的语句 ctrl + /: 注释 (选中要注释的行,然后用快捷键注释) ctrl + sh ...

  2. poj 3617 Best Cow Line 贪心模拟

    Best Cow Line Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 42701   Accepted: 10911 D ...

  3. Helm 架构【转】

    在实践之前,我们先来看看 Helm 的架构. Helm 有两个重要的概念:chart 和 release. chart 是创建一个应用的信息集合,包括各种 Kubernetes 对象的配置模板.参数定 ...

  4. CSS - flex使行内元素快速对齐

    div{ display:flex; alian-items:center; //使垂直对齐 justify-content:center //使水平对齐 }

  5. PreparedStatement 和 Statement 的区别(推荐使用PreparedStatement)

    PreparedStatement与Statement在使用时的区别: 1.Statement: String sql=" "; executeUpdate(sql) 2. Pre ...

  6. 7.8 Varnish 其他命令

  7. win上java1.7和1.8版本修改环境变量无效.md

    网上找了很多办法都没用. 解决办法: 看看自己 "系统环境变量" 中是不是有 "C:\ProgramData\Oracle\Java\javapath" 这项配 ...

  8. window 如何访问虚拟机的mapreduce(遇到的坑)

    首先 先把你虚拟机和本机网络链接弄通 (详情看上一篇)  一些关于mapreduce 和hadoop的配置都在上一篇 安装eclipse 的hadoop Map/Reduce插件详情 看其他博客园.. ...

  9. Golang的运算符-比较运算符

    Golang的运算符-比较运算符 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.比较运算符概述 比较运算符也称为关系运算符,比较运算符返回的类型为bool类型,常见的比较运算符 ...

  10. uni-app实现弹窗遮罩

    <template> <view> <view class="systemboxItem" @click="showSystemDialog ...