1、
如何骗过Spring Security——直接添加Authentication对象
1.需求
在最近的项目中,出现了这样的需求。我需要在后台使用spring security,但是android端显然不能使用像web端登录那样的处理方式,所以如何"骗过"spring security直接在它的认证流程中插入我自己的对象,这成为了我急切的问题。
2.Spring Security的核心组件
SecurityContextHolder
SecurityContextHolder用于获取当前用户的信息,SecurityContextHolder默认使用ThreadLocal来存储认证信息,这是一种与线程绑定的策略。只要在同一个线程中进行,即使不在各个方法之间以参数的形式传递,各个方法也能通过SecurityContextHolder工具获取到安全上下文。
获取当前的用户信息
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
Authentication
源码
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();//密码信息
Object getDetails();//细节信息,记录了ip地址和sessionId的值
Object getPrincipal();//大部分情况下返回UserDetais的实现类
boolean isAuthenticated();//是否认证
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
AuthenticationManager
正如他的名字一样,认证的管理者。他的作用不是直接亲自上场进行认证,而是委派其他有能力的AuthenticationProvider(实际上是实现此接口的实现类进行认证)进行认证,而AuthenticationProvider又由ProviderManager提供。在实际的需求中,我们登陆的方式不同,认证的方式就会不同,所以设计成这样后,如果我们需要自己定义登录方式,则只需要提供相应的AuthenticationProvider就可以了。
DaoAuthenticationProvider
提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource。
UserDetails与UserDetailsService
UserDetais源码
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetails是与UsernamePasswordAuthenticationToken比对的对象,由UserDetailsService获得。UserDetailsService是我们组装UserDetails的地方,我们也可以实现这个接口来完成自定义的组装。
3.Spring Security身份认证流程
- 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
- AuthenticationManager 身份管理器负责验证这个Authentication
- 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
- SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
4.将自己需要的Authentication对象放入Spring Security中
@RequestMapping("/doLogin")
public int doLogin(@RequestParam("userName") String userName,@RequestParam("password") String password,HttpServletRequest request){
User user = simpleUserService.getUserByName(userName);
//如果用户不存在则抛出异常
if(user==null){
throw new UsernameNotFoundException("没有当前用户");
}
else {
//如果用户存在且用户的密码相同,则在SecurityContextHolder.getContext().setAuthentication()放入authentication
if(user.getPassword().equals(password)){
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password, AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRole()));
authRequest.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authRequest);
return 1;
}
}
return 0;
}
2、
SpringBoot + SpringSecurity+jwt 实现验证
记录一下使用springSecurity实现jwt的授权方法,这方法可以实现权限的基本认证。当然这个案例还有许多的问题,不过还是先记录一下。其他功能以后在补充。
- 建议工程创建流程
- 创建 JwtTokenUtils
- 创建 jwtAccessDeniedHandler 和 JwtAuthenticationEntryPoint
- 创建 UserDetailsServiceImpl
- 创建 JwtAuthenticationFilter
- 配置 Security信息
- 启动类的信息
- springBoot 2.3.3
- springSecurity 5.0
- jjwt 0.91
pox.xml 文件主要信息
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</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>
请忽略文件命名
jwtAccessDeniedHandler 和 JwtAuthenticationEntryPoint
这两个类的作用是用户访问没有授权资源和携带错误token的错误返回处理信息类,要使用这两个类只需要在security的配置文件中配置一下就可以只用了
/**
* @author Bxsheng
* @blogAddress www.kdream.cn
* @createTIme 2020/9/17
* since JDK 1.8
* 当用户在没有授权的时候,返回的指定信息
*/
@Component
public class jwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
System.out.println("用户访问没有授权资源");
System.out.println(e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e==null?"用户访问没有授权资源":e.getMessage());
}
}
/**
* @author Bxsheng
* @blogAddress www.kdream.cn
* @createTIme 2020/9/17
* since JDK 1.8
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("用户访问资源没有携带正确的token");
System.out.println(e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e==null?"用户访问资源没有携带正确的token":e.getMessage());
}
}
该类直接继承UserDetailsService 进行登录信息验证,在输入账户密码进行登录的时候,会进入这个类进行验证信息。
当然我这里是直接使用了写死的密码,正常应该从数据库中获取用户的信息和权限信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//直接写死数据信息,可以在这里获取数据库的信息并进行验证
UserDetails userDetails = User.withUsername(s).password(new BCryptPasswordEncoder().encode("123456"))
.authorities("bxsheng").build();
return userDetails;
}
}
该类直接使用 slyh 的 [SpringBoot+JWT实现登录权限控制(代码))](( https://blog.csdn.net/sfh2018/article/details/104772986))的文章里面的类。
package cn.kdream.securityjwt.utlis;
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author Bxsheng
* @blogAddress www.kdream.cn
* @createTIme 2020/9/16
* since JDK 1.8
*/
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String SECRET = "jwtsecret";
public static final String ISS = "echisan";
private static final Long EXPIRATION = 60 * 60 * 3L; //过期时间3小时
private static final String ROLE = "role";
//创建token
public static String createToken(String username, String role, boolean isRememberMe){
Map map = new HashMap();
map.put(ROLE, 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中获取用户名(此处的token是指去掉前缀之后的)
public static String getUserName(String token){
String username;
try {
username = getTokenBody(token).getSubject();
} catch ( Exception e){
username = null;
}
return username;
}
public static String getUserRole(String token){
return (String) getTokenBody(token).get(ROLE);
}
private static Claims getTokenBody(String token){
Claims claims = null;
try{
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch(ExpiredJwtException e){
e.printStackTrace();
} catch(UnsupportedJwtException e){
e.printStackTrace();
} catch(MalformedJwtException e){
e.printStackTrace();
} catch(SignatureException e){
e.printStackTrace();
} catch(IllegalArgumentException e){
e.printStackTrace();
}
return claims;
}
//是否已过期
public static boolean isExpiration(String token){
try{
return getTokenBody(token).getExpiration().before(new Date());
} catch(Exception e){
System.out.println(e.getMessage());
}
return true;
}
}
JwtAuthenticationFilter 自定义验证jwt
该类直接使用 slyh 的 [SpringBoot+JWT实现登录权限控制(代码))](( https://blog.csdn.net/sfh2018/article/details/104772986))的文章里面的类。
这个类主要的作用是验证jwt信息 ,主要携带了token请求过来,解析jwt并设置在security的上下文中。这样做的其中一个目的是你获得了token中携带的权限信息,并保存在上下文中。你就可以对用户进行权限认证了
/**
* @author Bxsheng
* @blogAddress www.kdream.cn
* @createTIme 2020/9/16
* since JDK 1.8
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
public JwtAuthenticationFilter(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,则进行解析,并且设置认证信息
if(!JwtTokenUtils.isExpiration(tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX,""))){
//设置上下文
UsernamePasswordAuthenticationToken authentication = getAuthentication(tokenHeader);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
super.doFilterInternal(request, response, chain);
}
//获取用户信息
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader){
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
String username = JwtTokenUtils.getUserName(token);
// 获得权限 添加到权限上去
String role = JwtTokenUtils.getUserRole(token);
List<GrantedAuthority> roles = new ArrayList<GrantedAuthority>();
roles.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return role;
}
});
if(username != null){
return new UsernamePasswordAuthenticationToken(username, null,roles);
}
return null;
}
}
@EnableGlobalMethodSecurity(prePostEnabled = true) 开启prePostEnabled注解方式授权
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityJwtConfig extends WebSecurityConfigurerAdapter {
@Autowired
private jwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.OPTIONS,"/**")
.permitAll()
.antMatchers("/").permitAll()
//login 不拦截
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
//授权
.and()
// 禁用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 使用自己定义的拦截机制,拦截jwt
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
//授权错误信息处理
.exceptionHandling()
//用户访问资源没有携带正确的token
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
//用户访问没有授权资源
.accessDeniedHandler(jwtAccessDeniedHandler);
}
@Bean
public PasswordEncoder passwordEncoder(){
//使用的密码比较方式
return new BCryptPasswordEncoder();
}
}
我在启动类中配置了三个方法,一个是用来进行登录信息的,另外两个设置了需要权限访问
@SpringBootApplication
@RestController
public class SecurityJwtApplication {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
public SecurityJwtApplication(AuthenticationManagerBuilder authenticationManagerBuilder) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
}
public static void main(String[] args) {
SpringApplication.run(SecurityJwtApplication.class, args);
}
@GetMapping("/")
public String index(){
return "security jwt";
}
@PostMapping("/login")
public String login(@RequestParam String u,@RequestParam String p){
// 登陆验证
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(u, p);
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
//创建jwt信息
String token1 = JwtTokenUtils.createToken(u,"bxsheng", true);
return token1;
}
@GetMapping("/role")
@PreAuthorize("hasAnyAuthority('bxsheng')")
public String roleInfo(){
return "需要获得bxsheng权限,才可以访问";
}
@GetMapping("/roles")
@PreAuthorize("hasAnyAuthority('kdream')")
public String rolekdream(){
return "需要获得kdream权限,才可以访问";
}
}
效果
-
直接访问需要授权的用户信息
直接没有使用token直接访问只要授权的资源信息,会进入JwtAuthenticationEntryPoint 类
-
获取token
访问在启动类中的login方法,获取token信息
因为我使用了固定的密码,所以在使用错误的密码访问的时候,可以在springboot的全局异常处理中捕获到异常信息
/** * @author Bxsheng * @blogAddress www.kdream.cn * @createTIme 2020/9/17 * since JDK 1.8 */ @RestControllerAdvice public class Error { @ExceptionHandler(BadCredentialsException.class) public void badCredentialsException(BadCredentialsException e){ System.out.println(e.getMessage());//用户名或密码错误 // throw new BadCredentialsException(e.getMessage()); } }
-
正确的获取token,并进行受保护的资源访问
里面有写死的bxsheng权限信息,所以正常是可以获取bxsheng标识的资源信息的。
- 成功获取信息
-
尝试获取无权限资源信息
使用token直接访问无权限资源信息,会进入jwtAccessDeniedHandler 类
至此应该可以说算完成简单的权限管理了。