SpringBoot+JPA+SpringSeurity+JWT
目的:使用这个框架主要就是为了解决高并发环境下登陆操作对数据库及服务器的压力,同时能保证安全性;
- 加载时,SpringSecurity定义拦截器和添加两个Fitler;
- 登陆时,登陆成功,通过传入的信息(例如:用户名+密码)authenticationManager.authenticate()进行认证得到Authentication;
- 认证成功JWT根据规则生成Token(Bearer空格 + Token) 存到Header;认证失败直接抛出提示;
- 执行操作时鉴权。拿到Header,如果没拿到就放行了;
- 拿到了后,先判断Token是否失效,然后解析出用户名,角色,执行SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken);再次设置认证信息;没设置成功报403,最后放行。
pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.1-jre</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
User.java
import lombok.Getter; import lombok.Setter; import lombok.ToString; import javax.persistence.*; @Entity @Table(name = "jd_user") @Getter @Setter @ToString public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Integer id; @Column(name = "username") private String username; @Column(name = "password") private String password; @Column(name = "role") private String role; }
JwtUser.java
import lombok.AllArgsConstructor; import lombok.ToString; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Collections; @AllArgsConstructor @ToString public class JwtUser implements UserDetails { private Integer id; private String username; private String password; private Collection<? extends GrantedAuthority> authorities; // 写一个能直接使用user创建jwtUser的构造器 public JwtUser(User user) { id = user.getId(); username = user.getUsername(); password = user.getPassword(); authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole())); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
LoginUser.java
import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @Getter @Setter public class LoginUser { private String username; private String password; private Integer rememberMe; }
UserRepository.java
import com.hz.entity.User; import org.springframework.data.repository.CrudRepository; public interface UserRepository extends CrudRepository<User, Integer> { User findByUsername(String username); }
UserDetailsServiceImpl.java
import com.hz.entity.JwtUser; import com.hz.entity.User; import com.hz.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; /** * Created by echisan on 2018/6/23 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { User user = userRepository.findByUsername(s); return new JwtUser(user); } }
JwtTokenUtils.java
import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.HashMap; /** * Created by echisan on 2018/6/23 */ public class JwtTokenUtils { public static final String TOKEN_HEADER = "Authorization"; public static final String TOKEN_PREFIX = "Bearer "; private static final String SECRET = "jwtsecretdemo"; private static final String ISS = "echisan"; // 角色的key private static final String ROLE_CLAIMS = "rol"; // 过期时间是3600秒,既是1个小时 private static final long EXPIRATION = 3600L; // 选择了记住我之后的过期时间为7天 private static final long EXPIRATION_REMEMBER = 604800L; // 创建token public static String createToken(String username,String role, boolean isRememberMe) { long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION; HashMap<String, Object> map = new HashMap<>(); map.put(ROLE_CLAIMS, role); return Jwts.builder() .signWith(SignatureAlgorithm.HS512, SECRET) .setClaims(map) .setIssuer(ISS) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) .compact(); } // 从token中获取用户名 public static String getUsername(String token){ return getTokenBody(token).getSubject(); } // 获取用户角色 public static String getUserRole(String token){ return (String) getTokenBody(token).get(ROLE_CLAIMS); } // 是否已过期 public static boolean isExpiration(String token) { try { return getTokenBody(token).getExpiration().before(new Date()); } catch (ExpiredJwtException e) { return true; } } private static Claims getTokenBody(String token){ return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }
SecurityConfig.java
import com.hz.exception.JWTAccessDeniedHandler; import com.hz.exception.JWTAuthenticationEntryPoint; import com.hz.filter.JWTAuthenticationFilter; import com.hz.filter.JWTAuthorizationFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; /** * Created by echisan on 2018/6/23 */ @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .authorizeRequests() .antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN") // 测试用资源,需要验证了的用户才能访问 .antMatchers("/tasks/**").authenticated() // 其他都放行了 .anyRequest().permitAll() .and() .addFilter(new JWTAuthenticationFilter(authenticationManager())) .addFilter(new JWTAuthorizationFilter(authenticationManager())) // 不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint()) .accessDeniedHandler(new JWTAccessDeniedHandler()); //添加无权限时的处理 } @Bean CorsConfigurationSource corsConfigurationSource() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); return source; } }
JWTAuthenticationFilter.java(校验)
import com.hz.entity.JwtUser; import com.hz.model.LoginUser; import com.hz.utils.JwtTokenUtils; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; /** * Created by echisan on 2018/6/23 */ public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private ThreadLocal<Integer> rememberMe = new ThreadLocal<>(); private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 从输入流中获取到登录的信息 try { LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class); rememberMe.set(loginUser.getRememberMe()); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>()) ); } catch (Exception e) { e.printStackTrace(); return null; } } // 成功验证后调用的方法 // 如果验证成功,就生成token并返回 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { JwtUser jwtUser = (JwtUser) authResult.getPrincipal(); System.out.println("jwtUser:" + jwtUser.toString()); boolean isRemember = rememberMe.get() == 1; String role = ""; Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities(); for (GrantedAuthority authority : authorities){ role = authority.getAuthority(); } String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember); // String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false); // 返回创建成功的token // 但是这里创建的token只是单纯的token // 按照jwt的规定,最后请求的时候应该是 `Bearer token` response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token); } @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.getWriter().write("authentication failed, reason: " + failed.getMessage()); } }
JWTAuthorizationFilter(鉴权)
import com.hz.exception.TokenIsExpiredException; import com.hz.utils.JwtTokenUtils; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; /** * Created by echisan on 2018/6/23 */ public class JWTAuthorizationFilter extends BasicAuthenticationFilter { public JWTAuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER); // 如果请求头中没有Authorization信息则直接放行了 if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) { chain.doFilter(request, response); return; } // 如果请求头中有token,则进行解析,并且设置认证信息 try { SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader)); } catch (TokenIsExpiredException e) { //返回json形式的错误信息 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); String reason = "统一处理,原因:" + e.getMessage(); response.getWriter().write(new ObjectMapper().writeValueAsString(reason)); response.getWriter().flush(); return; } super.doFilterInternal(request, response, chain); } // 这里从token中获取用户信息并新建一个token private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws TokenIsExpiredException { String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, ""); boolean expiration = JwtTokenUtils.isExpiration(token); if (expiration) { throw new TokenIsExpiredException("token超时了"); } else { String username = JwtTokenUtils.getUsername(token); String role = JwtTokenUtils.getUserRole(token); if (username != null) { return new UsernamePasswordAuthenticationToken(username, null, Collections.singleton(new SimpleGrantedAuthority(role)) ); } } return null; } }
异常处理1:TokenIsExpiredException.java
/** * @description: 自定义异常 */ public class TokenIsExpiredException extends Exception { public TokenIsExpiredException() { } public TokenIsExpiredException(String message) { super(message); } public TokenIsExpiredException(String message, Throwable cause) { super(message, cause); } public TokenIsExpiredException(Throwable cause) { super(cause); } public TokenIsExpiredException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
异常处理2:
JWTAuthenticationEntryPoint.java
import com.fasterxml.jackson.databind.ObjectMapper; 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; /** * @description:没有携带token或者token无效 */ public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); String reason = "统一处理,原因:" + authException.getMessage(); response.getWriter().write(new ObjectMapper().writeValueAsString(reason)); } }
异常处理3:JWTAccessDeniedHandler.java
import com.fasterxml.jackson.databind.ObjectMapper; 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; /** * @description:没有访问权限 */ public class JWTAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json; charset=utf-8"); httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); String reason = "统一处理,原因:" + e.getMessage(); httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(reason)); } }
AuthController.java
import com.hz.entity.User; import com.hz.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * Created by echisan on 2018/6/23 */ @RestController @RequestMapping("/auth") public class AuthController { @Autowired private UserRepository userRepository; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @PostMapping("/register") public String registerUser(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password) { User user = new User(); user.setUsername(username); user.setPassword(bCryptPasswordEncoder.encode(password)); user.setRole("ROLE_USER"); User save = userRepository.save(user); return save.toString(); } }
TaskController.java
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; /** * Created by echisan on 2018/6/23 */ @RestController @RequestMapping("/tasks") public class TaskController { @GetMapping public String listTasks(){ return "任务列表"; } @PostMapping @PreAuthorize("hasRole('ADMIN')") public String newTasks(){ return "创建了一个新的任务"; } @PutMapping("/{taskId}") public String updateTasks(@PathVariable("taskId")Integer id){ return "更新了一下id为:"+id+"的任务"; } @DeleteMapping("/{taskId}") public String deleteTasks(@PathVariable("taskId")Integer id){ return "删除了id为:"+id+"的任务"; } }
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false spring.datasource.username=root spring.datasource.password=root spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update
测试:postman
注册URL : http://localhost:8080/auth/register?username=admin1&password=admin1
说明:这一步没有什么重要作用,只是向数据库中注册了一条用户信息;
登陆URL:http://localhost:8080/login(注意Body — raw 手动填写JSON)
说明:Header中加入token
操作的URL:http://localhost:8080/tasks/11
说明:先Authorization的TYPE为NO AUTH(不加token),如图所示,403异常
操作的URL:http://localhost:8080/tasks/11
说明:把Authorization的TYPE为Bearer Auth,如图所示,访问正常