springboot+springsecurity+JWT
目录结构:
springbootApplication里面的配置,如有需要可以自行添加
实现JWT功能
在pom.xml中的配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<groupId>com.my</groupId>
<artifactId>my-springsecurity</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<!-- oracle数据库驱动 -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.30</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- <!– 资源文件拷贝插件 –>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>-->
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在application.yml中的配置
server:
port: ${PORT:8080}
spring:
application:
name: my-springsecurity
datasource:
url: jdbc:oracle:thin:@localhost:1521:orcl
username: scott
password: oracle
driver-class-name: oracle.jdbc.driver.OracleDriver
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
timeout: 5000 #连接超时 毫秒
# main:
# allow-bean-definition-overriding: true #同名bean覆盖
auth:
redisAge: 600 #单位秒
# 无法扫描到resources下的templates中的静态文件时可以配置,如果可以无需配置
#注入到TomcatConfig
bw:
factory:
doc:
root: E:\JAVAIDEA\My\springboot\my-springsecurity\src\main\resources\templates
首先需要继承WebSecurityConfigurerAdapter类,配置security中的认证授权相关配置
SecurityConfig 类
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//token 过滤器,解析token
@Autowired
MyJwtTokenFilter jwtTokenFilter;
@Autowired
MyUserDetailsService myUserDetailsService;
@Autowired
private SendSmsSecurityConfig sendSmsSecurityConfig;
//加密机制
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// .antMatchers("/admin/api/**").hasRole("ADMIN")
// .antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/SendSms").permitAll()
.antMatchers("/userlogout").permitAll()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()//任何请求登录后访问
.and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .sessionManagement() //会话管理
// .maximumSessions(1); //限制登录人数
.and().csrf().disable();
//登录操作
http.formLogin()
.loginPage("/login.html")//登录路径
.loginProcessingUrl("/farmerlogin")//登录表单提交请求
.usernameParameter("username")//设置登录账号参数,默认username
.passwordParameter("password")//设置登录密码参数,默认password
// .defaultSuccessUrl("/home.html")//登录成功跳转,不能和successHandler一起使用
// .failureUrl("/login.html")// 登录失败跳转,不能和failureHandler一起使用
.permitAll()
//登录成功处理
.successHandler(new LoginSuccessHandler())
//登录失败处理
.failureHandler(new LoginFailureHandler())
.and().apply(sendSmsSecurityConfig);
//退出操作
http.logout()
.logoutUrl("/aaa")//退出提交参数
.logoutSuccessUrl("/login.html");//退出后跳转,不能和logoutSuccessHandler一起使用
// .logoutSuccessHandler();//退出处理
// 禁用缓存
http.headers().cacheControl();
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 添加权限不足 filter
.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler())
//其他异常处理类
.authenticationEntryPoint(new MyAuthenticationException());
}
//忽略拦截
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/elementuidemo/**",
"/img/**", "/js/**", "/plugins/**", "/static/json/**", "/pages/**");
}
}
配置相关配件:
LoginSuccessHandler 类(登陆成功处理)
登录成功处理:继承SavedRequestAwareAuthenticationSuccessHandler类,该类继承AuthenticationSuccessHandler类,AuthenticationSuccessHandler登录成功父类,之所以用SavedRequestAwareAuthenticationSuccessHandler类,里面可以实现其他的方法,如:登录成功跳转到登录之前请求的页面
/**
* <p>
* AuthenticationSuccessHandler登录成功父类
* SavedRequestAwareAuthenticationSuccessHandler 继承AuthenticationSuccessHandler
* <p>
* 登录成功处理
*/
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Value("${auth.redisAge}")
int redisAge;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功");
//从authentication中获取用户信息
final User farmerDetail = (User) authentication.getPrincipal();
//生成jwt
String token = jwtTokenUtil.generateToken(farmerDetail);
httpServletResponse.addHeader("token", "Bearer " + token);
//把token保存到redis中,farmerDetail.getUsername()用户名作为key,也可以用id作为key值farmerDetail.getId()
stringRedisTemplate.opsForValue().set(farmerDetail.getUsername(),token,redisAge, TimeUnit.SECONDS);
httpServletResponse.setContentType("application/json");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.getWriter().write("{\"result\":\"ok\"}");
}
}
LoginFailureHandler 类(登录失败处理)
登录失败处理:继承SimpleUrlAuthenticationFailureHandler类,该类继承SimpleUrlAuthenticationFailureHandler类,SimpleUrlAuthenticationFailureHandler类登录失败父类,之所以用SimpleUrlAuthenticationFailureHandler类,里面可以实现其他方法,如:登录失败跳转到登录页面
/**
*
* AuthenticationFailureHandler登录失败父类
* SimpleUrlAuthenticationFailureHandler 继承AuthenticationFailureHandler
*
* 登录失败处理
*/
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(401);
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"error_code\":\"401\",\"name\":\""+e.getClass()+"\",\"message\":\""+e.getMessage()+"\"}");
}
}
MyAccessDeniedHandler 类(权限不足处理)
权限不足处理:实现的AccessDeniedHandler类,进行的权限校验
/**
* Spring security权限不足处理类
* 只有登录后(即接口有传token)接口权限不足才会进入AccessDeniedHandler,
* 如果是未登陆或者会话超时等,不会触发AccessDeniedHandler,而是会直接跳转到登陆页面。
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//登陆状态下,权限不足执行该方法
System.out.println("权限不足:" + e.getMessage());
response.setStatus(200);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
response.getWriter().write("{\"result\":\"权限不足\"}");
printWriter.flush();
}
}
MyAuthenticationException 类(异常处理)
异常处理:实现AuthenticationEntryPoint类,实现异常的处理,如果不配置此类,则spring security默认会跳转到登录页面
/**
* Spring security其他异常处理类,比如请求路径不存在等,
* 如果不配置此类,则Spring security默认会跳转到登录页面
*/
@Component
public class MyAuthenticationException implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("AuthenticationEntryPoint检测到异常:"+e);
httpServletResponse.setStatus(200);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = httpServletResponse.getWriter();
httpServletResponse.getWriter().write("AuthenticationEntryPoint检测到异常:"+e);
printWriter.flush();
}
}
MyUserDetailsService 类(从数据库查询用户信息)
UserDetailsService验证用户名、密码和授权处理,实现从数据库查询用户功能(仅用于测试,用户和权限在一张表中,根据需求可以进行多表查询赋予权限)
/**
* 根据账号查询用户
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//从数据库读取该用户
User user = userMapper.findByUserName(username);
// 用户不存在,抛出异常
if (user == null){
throw new UsernameNotFoundException("用户不存在");
}
//将数据库形式的roles解析为UserDtails的权限集
// farmer.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(farmer.getRoles()));
List<GrantedAuthority> authorities = generateAuthorities(user.getRoles());
user.setAuthorities(authorities);
return user;
//基于内存验证用户信息
// UserDetails userDetails = Farmer.withUsername("123").password("123").roles("USER").build();
// return userDetails;
}
//自定义实现权限转换
private List<GrantedAuthority> generateAuthorities(String roles){
List<GrantedAuthority> authorities = new ArrayList<>();
String[] roleArray = roles.split(",");
if (roles != null && !"".equals(roles)){
for (String role : roleArray) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
return authorities;
}
}
MyJwtTokenFilter 类(token过滤器)
token过滤器进行解析判断是否登录,继承OncePerRequestFilter类
/**
* token 过滤器,在这里解析token,拿到该用户角色,设置到springsecurity的上下文环境中,让springsecurity自动判断权限
* 所有请求最先进入此过滤器,包括登录接口,而且在springsecurity的密码验证之前执行
*/
@Component
public class MyJwtTokenFilter extends OncePerRequestFilter {
@Autowired
MyUserDetailsService myUserDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if (authHeader != null && authHeader.startsWith(tokenHead)) {
String token = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
//从redis中取出存储的token
String redisToken = stringRedisTemplate.opsForValue().get(userDetails.getUsername());
//验证令牌是否有效
if (token.equals(redisToken) && jwtTokenUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
JwtTokenUtil 类(JWT工具类)
JWT工具类
@Component
public class JwtTokenUtil implements Serializable {
/**
* 密钥
*/
private final String secret = "aaaa";
@Value("${auth.redisAge}")
private Long redisAge;
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + redisAge * 1000);
}
/**
* 从数据声明生成令牌
* @param claims 数据声明
* @return 令牌
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return generateToken(claims);
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public 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 Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public 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 令牌
* @param userDetails 用户
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
User 类(实现UserDetails类)
在pojo文件中,写User实体类实现UserDetails类
public class User implements UserDetails{
private int id;
private String username;
private String password;
private String roles;
private List<GrantedAuthority> authorities;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles;
}
public void setAuthorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
//账号是否没过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账号是否没被锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//密码是否没过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账号是否可用
@Override
public boolean isEnabled() {
return true;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", roles='" + roles + '\'' +
", authorities=" + authorities +
'}';
}
}
dao层,mapper.xml,数据库可以进行简单的配置进行测试。数据库为Oracle,可以自行配置mysql或Oracle
数据库就四个字段,id,username,password,权限。
controller中有三个测试类
@RestController
@RequestMapping("/admin/api")
public class AdminController {
@GetMapping("hello")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String hello(){
return "hello,admin";
}
}
@RestController
@RequestMapping("/app/api")
public class AppController {
@GetMapping("hello")
public String hello(){
return "hello,app";
}
}
@RestController
@RequestMapping("/user/api")
public class UserController {
@GetMapping("/hello")
@PreAuthorize("hasAuthority('ROLE_USER')")
public String hello(){
return "hello,user";
}
}
在Postman中进行登录测试
登录成功显示: farmerlogin是在SecurityConfig中自定义的,默认为login
会在Headers中生成一个token
这个token是自己定义的,在登录成功处理类中生成的token令牌
在验证时,在Key中存放 Authorization ,在Value中把生成的token保存进去,然后会在MyJwtTokenFilter类中进行令牌校验
成功示例:
退出登录时
/**
* 退出操作
*/
@RestController
@RequestMapping("/")
public class LogoutController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 退出
* @param username 账号,存入redis中的key值
*/
@RequestMapping("/userlogout")
public void logout(@RequestParam("username") String username) {
//删除redis中的key
stringRedisTemplate.delete(username);
}
}
SendSmsSecurityConfig类为短信登录的配置类,须在SecurityConfig进行声明
SendSms文件中的类为短信登录相关的配置,和本章并无冲突,遇到相关配置可直接删除
test测试文件里面是发送短信测试
TomcatConfig类不重要,需要了可以配置:
TomcatConfig类的配置说明 https://blog.csdn.net/weixin_45498999/article/details/105973351
代码资源:https://download.csdn.net/download/weixin_45498999/12500298