本案例是在上个案例的基础上改编的,由于是前后端分离,自动登录功能已去掉。功能不变,只是响应的页面改成api接口
环境搭建
springboot 版本2.4.1
导入额外的jwt坐标
本案例中没有使用thymeleaf相关jar包
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.7</version>
</dependency>
Spring Data Jpa配置
和上个案例相同,其中还省去了自动登录的表
Spring security配置
jwt工具类
public class JJWTUtils {
// 自定义设置签名 最少32位
// private static final SecretKey key = Keys.hmacShaKeyFor("1234567890_1234567890_1234567890".getBytes());
// 每次运行程序都会随机生成签名
private static final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public static String getToken(Map<String, Object> map) {
Calendar c = Calendar.getInstance();
// token有效期7天
c.add(Calendar.DATE,7);
String token = Jwts.builder()
.setClaims(map)
.setExpiration(c.getTime())
.signWith(key).compact();
return token;
}
public static Claims getClaims(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return claims;
}
}
自定义实现AuthenticationSuccessHandler接口
该接口用于处理登录成功时的操作,此时用来返回json数据
@Slf4j
@Component("authenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@SneakyThrows
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("认证成功:【{}】",authentication.toString());
HashMap<String, Object> map = new HashMap<>();
map.put("username",authentication.getName());
List<GrantedAuthority> authorities = (List<GrantedAuthority>) authentication.getAuthorities();
List<String> list = authorities.stream().map(g -> g.getAuthority()).collect(Collectors.toList());
map.put("role", list);
String token = "Bearer "+ JJWTUtils.getToken(map);
ServletUtil.setHeader(response, "Authorization", token);
JSONObject obj = JSONUtil.createObj();
obj.putOpt("code", 200);
obj.putOpt("msg", "认证通过");
obj.putOpt("Authorization", token);
obj.putOpt("user", map);
ServletUtil.write(response, obj.toString(),"application/json;charset=utf-8");
}
}
自定义实现AuthenticationFailureHandler接口
该接口用于处理登录失败时的操作,例如返回用户名密码错误的json数据
@Slf4j
@Component("authenticationFailureHandler")
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.info("认证失败:【{}】",e.getMessage());
HashMap<Object, Object> map = new HashMap<>();
map.put("code", 500);
if (e instanceof UsernameNotFoundException) {
map.put("msg", e.getMessage());
} else if (e instanceof BadCredentialsException) {
map.put("msg", "密码错误"); //默认为:Bad credentials,友好提示
}
ServletUtil.write(httpServletResponse, JSONUtil.toJsonPrettyStr(map),"application/json;charset=utf-8");
}
}
自定义实现AuthenticationEntryPoint接口
该接口用于处理没有登录时的操作,例如提示未登录的json数据
@Slf4j
@Component("authenticationEntryPoint")
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) {
log.info("AuthenticationException:【{}】",e.toString());
JSONObject jsonObject = JSONUtil.createObj();
jsonObject.putOpt("code", 500);
jsonObject.putOpt("msg", "请登陆!");
ServletUtil.write(httpServletResponse,jsonObject.toString(),"application/json;charset=utf-8");
}
}
自定义实现AccessDeniedHandler接口
该接口用来处理权限不足的操作
@Slf4j
@Component("accessDeniedHandler")
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("权限不足:【{}】",e.getMessage());
JSONObject jsonObject = JSONUtil.createObj();
jsonObject.putOpt("code", 403);
jsonObject.putOpt("msg", "权限不足无法访问");
ServletUtil.write(httpServletResponse,jsonObject.toString(),"application/json;charset=utf-8");
}
}
自定义过滤器用于读取header中的token信息
并根据token不同的错误信息返回响应的json数据
@Slf4j
public class JwtVerifyFilter extends BasicAuthenticationFilter {
private ObjectMapper objectMapper = new ObjectMapper();
public JwtVerifyFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
// 从header获取token
String header = request.getHeader("Authorization");
if (Validator.isEmpty(header)) {
// 如果获取不到就继续从请求参数中获取token
header = request.getParameter("Authorization");
}
if (header == null || !header.startsWith("Bearer ")) {
// 不论成功与否都要放行确保过滤器链不断
chain.doFilter(request, response);
} else {
String token = header.replace("Bearer ", "");
try {
// 获取token中的用户名和角色信息
Claims claims = JJWTUtils.getClaims(token);
ArrayList<String> role = (ArrayList<String>) claims.get("role");
log.info("role:{}", role);
UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(claims.get("username"), null, AuthorityUtils.createAuthorityList(role.toArray(new String[role.size()])));
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
} catch (MalformedJwtException e) {
sendMsg(response, "token无效");
} catch (ExpiredJwtException e) {
sendMsg(response, "toke过期");
} catch (JwtException e) {
sendMsg(response, "未知错误[" + e.toString() + "]");
}
}
}
@SneakyThrows
public void sendMsg(HttpServletResponse response, String msg) {
response.reset();
HashMap<String, Object> map = new HashMap<>();
map.put("code", 500);
map.put("msg", msg);
ServletUtil.write(response, objectMapper.writeValueAsString(map), "application/json;charset=utf-8");
}
}
spring security配置类
把上面的自定义类都配置好
@Configuration
@Slf4j
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) //开启security注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
// 认证失败的操作
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
// 认证成功的操作
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
// 为登录的操作
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
// 权限不足的操作
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Autowired
private AuthenticationProvider authenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 自定义实现类,此时无论用户名不存在还是密码错误都提示密码错误
// auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
auth.authenticationProvider(authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf
http.csrf().disable();
// 登录成功和登录失败的操作
http.formLogin().successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler);
// 未登录和权限不足时的操作
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);
// 除/hello外(测试),拦截所有路径
http.authorizeRequests().antMatchers("/hello").permitAll();
// 添加自定义的过滤器
http.addFilter(new JwtVerifyFilter(authenticationManager()));
// 关闭session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/*
* 密码加密器
* */
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/*
* 登录的友好提示,为了区分用户名不存在和密码错误
* */
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 显示用户找不到异常
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(userDetailsService);
return provider;
}
}
MVC配置
controller编写
和上一个案例没什么区别,就是修改一下返回的数据成了json形式
@RestController
@Slf4j
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/role/teacher")
@Secured({"ROLE_teacher", "ROLE_admin"})
public Object teacher() {
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
ArrayList<String> arrayList = new ArrayList<>();
for (GrantedAuthority authority : authorities) {
arrayList.add(authority.getAuthority());
}
HashMap<Object, Object> map = new HashMap<>();
map.put("role", arrayList);
map.put("msg", "教师界面");
return map;
}
@GetMapping("/role/admin")
@Secured({"ROLE_admin"})
public Object admin() {
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
ArrayList<String> arrayList = new ArrayList<>();
for (GrantedAuthority authority : authorities) {
arrayList.add(authority.getAuthority());
}
HashMap<Object, Object> map = new HashMap<>();
map.put("role", arrayList);
map.put("msg", "管理员界面");
return map;
}
@GetMapping("/role/student")
@PreAuthorize("hasAnyRole('student','admin')")
public Object student() {
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
ArrayList<String> arrayList = new ArrayList<>();
for (GrantedAuthority authority : authorities) {
arrayList.add(authority.getAuthority());
}
HashMap<Object, Object> map = new HashMap<>();
map.put("role", arrayList);
map.put("msg", "学生界面");
return map;
}
}