一、搭建环境
java中常用的权限管理框架有 shiro 和 spring security,之前一直在用 shiro 管理权限,但是后来发现 shiro 确实和前后端分离不太搭,就来研究了两天spring security,与 shiro 不同的是,spring security 是通过一系列的 过滤链管理权限的,而且这些过滤器都可以自定义,虽然比 shiro 体量更大,但是更加的灵活,可以高度自定义,而且 spring security 还会自动生成 login 接口。
1.1 导入依赖和基本配置
<dependencies>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</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>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.3</version>
<scope>compile</scope>
</dependency>
</dependencies>
server:
port: 9999
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: {your url}
username: {your username}
password: {your password}
mybatis-plus:
mapper-locations: classpath:com/gewj/mapper/xml/*
1.2、 数据库和简单实体类创建
create table user
(
id bigint auto_increment comment 'userId'
primary key,
username varchar(50) not null comment '用户名',
password varchar(1024) not null comment '密码',
nickname varchar(50) null comment '昵称',
telepnone varchar(20) null,
email varchar(30) null comment '邮箱',
role varchar(10) default 'ROLE_USER' null
);
创建实体类可以用 mybatis-plus 的代码生成器,也可以手动写,此处就不赘述啦
需要注意的是,userService 里面最好有一个 getUserByUsername() 的方法
public User getByUsername(String username) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
User one = getOne(wrapper);
return one;
}
1.3、 创建 UserDetails 类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDetials extends User implements UserDetails {
private Long id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private boolean isAccountNonExpired;
private boolean isAccountNonLocked;
private boolean isCredentialsNonExpired;
private boolean isEnabled;
public UserDetials(User user, Collection<? extends GrantedAuthority> authorities) {
this.setUsername(user.getUsername());
this.setId(user.getId());
this.setPassword(user.getPassword());
this.setAuthorities(authorities);
this.setAccountNonExpired(true);
this.setAccountNonLocked(true);
this.setCredentialsNonExpired(true);
this.setEnabled(true);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.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;
}
}
这个类的作用是提供给 spring security的。
Result 类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private Integer code;
private String message;
private Boolean success;
private Map<String, Object> data = new HashMap<>();
public static Result ok(String message) {
Result result = new Result();
result.setCode(200);
result.setMessage(message);
result.setSuccess(true);
return result;
}
public static Result error(Integer code, String message) {
Result result = new Result();
result.setSuccess(false);
result.setCode(code);
result.setMessage(message);
return result;
}
public static Result ok() {
Result result = new Result();
result.setCode(200);
result.setSuccess(true);
return result;
}
public static Result error() {
Result result = new Result();
result.setCode(500);
return result;
}
public static Result error(Integer code) {
Result result = new Result();
result.setCode(code);
return result;
}
public Result message(String message) {
this.setMessage(message);
return this;
}
public Result data(Map<String, Object> map) {
this.setData(map);
return this;
}
public Result data(String key, Object value) {
this.data.put(key, value);
return this;
}
public Result(Integer code) {
this.code = code;
}
public Result code(Integer code) {
this.setCode(code);
return this;
}
}
1.4、 jwt 工具类
@Component
public class JwtUtil {
private static final int EXPIRE_TIME_MINUTE = 300;
private static final String secret = "zlgewj";
public static String sign(String username, String authorities) {
Date date = DateUtil.offsetMinute(new Date(),EXPIRE_TIME_MINUTE);
String jwt = JWT.create()
.withClaim("username",username)
.withClaim("currentTimeMillis", String.valueOf(date))
.withClaim("authorities",authorities)
.withExpiresAt(date)
.sign(Algorithm.HMAC256(secret));
return jwt;
}
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
public static boolean verify(String token, String username) {
try {
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(secret))
.withClaim("username", username)
.build()
.verify(token);
return true;
}catch (Exception e) {
return false;
}
}
public static String getClaim(String token, String claimName) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(claimName).asString();
}catch (JWTDecodeException e){
e.printStackTrace();
return null;
}
}
}
二、 filter 和 handler
下面正头戏才开始:
2.1、 登录过滤器
@Slf4j
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
log.info("执行了 attemptAuthentication 方法");
UserDetials userDetials = new ObjectMapper().readValue(httpServletRequest.getInputStream(), UserDetials.class);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userDetials.getUsername(),
userDetials.getPassword(),
userDetials.getAuthorities()
);
return getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("执行了登陆成功回调");
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
StringBuffer stringBuffer = new StringBuffer();
authorities.forEach(authority -> {
stringBuffer.append(authority.getAuthority()).append(",");
});
String sign = JwtUtil.sign(authResult.getName(),stringBuffer.toString());
response.setContentType("application/json; charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
UserVo userVo = new UserVo();
userVo.setToken(sign);
Result result = Result.ok().data("user", userVo).message("登陆成功!");
outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setContentType("application/json; charset=UTF-8");
log.info("执行了登录失败回调");
Result res = Result.error(403, "账号或密码错误~");
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(res).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
发送的登录请求会来到 AbstractAuthenticationProcessingFilter 这个过滤器,我们实现了这个过滤器,调用getAuthenticationManager().authenticate(token); 这个时候,token 里面还没有 authorities, spring security 会用 UserDetailsService 查询用户的详细信息
2.2、 授权过滤器
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("USER_TOKEN");
Authentication authentication = null;
if (token != null) {
List<SimpleGrantedAuthority> authorities = Arrays.stream(JwtUtil.getClaim(token, "authorities").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()
);
String username = JwtUtil.getUsername(token);
log.info("认证过滤器执行了,当前用户权限:{}",authorities.toString());
if (username != null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, authorities);
usernamePasswordAuthenticationToken.setDetails(token);
if (!JwtUtil.verify(token, username)) {
log.info("token验证失败~");
response.setContentType("application/json; charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
Result error = Result.error(403, "登录过时,请重新登录~");
outputStream.write(JSONUtil.toJsonStr(error).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
authentication = usernamePasswordAuthenticationToken;
}
}
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
这个方法会根据请求头里 token 的 权限列表拿出来,放到全局,用我们 JwtUtil.verify 方法认证
2.3、权限不足的处理
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json; charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
Result error = Result.error(403, "权限不足~");
outputStream.write(JSONUtil.toJsonStr(error).getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
当请求的权限不足的时候就会来到这里
2.4、 获取 UserDetails 的 service
@Slf4j
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
private static UserService userService;
@Autowired
public void setUserService(UserService userService1) {
userService = userService1;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User byUsername = userService.getByUsername(username);
if ( null == byUsername) {
throw new UsernameNotFoundException("用户名或密码不正确");
}
UserDetials userDetials = new UserDetials(byUsername,getUserAuthority(byUsername.getId()));
System.out.println(userDetials.toString());
return userDetials;
}
public List<GrantedAuthority> getUserAuthority(Long userId){
User user = userService.getById(userId);
String role = user.getRole();
return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
}
}
执行 getAuthenticationManager().authenticate(token); 方法的时候 loadUserByUsername() 这个方法就会被调用,所以我们需要重写这个方法
三、配置类
刚刚我们写的 filter 和 handler,spring security 只知道它们的存在,不知道该不该调用,什么时候调用,所以
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static String ADMIN = "ROLE_ADMIN";
public static String USER = "ROLE_USER";
@Autowired
private MyLogoutHandler myLogoutHandler;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
private final static String[] PERMIT_ALL_MAPPING = {
"/api/hello",
"/api/login",
"/api/home",
"/api/verifyImage",
"/api/image/verify",
"/images/**"
};
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
List<String> allowedOriginsUrl = new ArrayList<>();
allowedOriginsUrl.add("*");
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(allowedOriginsUrl);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(PERMIT_ALL_MAPPING)
.permitAll()
.antMatchers("/api/user/**", "/api/data", "/api/logout")
.hasAnyAuthority(USER, ADMIN)
.antMatchers("/api/admin/**")
.hasAnyAuthority(ADMIN)
.anyRequest()
.authenticated()
.and()
.addFilterBefore(new JwtLoginFilter("/api/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.cors()
.and()
.csrf()
.disable()
.logout()
.addLogoutHandler(myLogoutHandler)
.and()
.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
authenticationProvider(daoAuthenticationProvider());
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(new UserDetailsServiceImpl());
return provider;
}
}
再来个测试接口测试一下
@RestController()
@RequestMapping("/api")
public class TestController {
@GetMapping("/hello")
@Secured({"ROLE_ADMIN"})
public String hello() {
return "hello";
}
}
|