Spring Boot 集成教程
- Spring Boot 介绍
- Spring Boot 开发环境搭建(Eclipse)
- Spring Boot Hello World (restful接口)例子
- spring boot 连接Mysql
- spring boot配置druid连接池连接mysql
- spring boot集成mybatis(1)
- spring boot集成mybatis(2) – 使用pagehelper实现分页
- spring boot集成mybatis(3) – mybatis generator 配置
- spring boot 接口返回值封装
- spring boot输入数据校验(validation)
- spring boot rest 接口集成 spring security(1) – 最简配置
- spring boot rest 接口集成 spring security(2) – JWT配置
- spring boot 异常(exception)处理
- spring boot 环境配置(profile)切换
- spring boot redis 缓存(cache)集成
在教程 [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