前言

主要实现 Spring Security 的安全认证,结合 RESTful API 的风格,使用无状态的环境。

主要实现是通过请求的 URL ,通过过滤器来做不同的授权策略操作,为该请求提供某个认证的方法,然后进行认证,授权成功返回授权实例信息,供服务调用。

基于Token的身份验证的过程如下:

  1. 用户通过用户名和密码发送请求。
  2. 程序验证。
  3. 程序返回一个签名的token 给客户端。
  4. 客户端储存token,并且每次用于每次发送请求。
  5. 服务端验证token并返回数据。

每一次请求都需要token,所以每次请求都会去验证用户身份,所以这里必须要使用缓存,

基本使用

加入相关依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

了解基础配置

认证的基本信息
public interface UserDetails extends Serializable {

    //返回分配给用户的角色列表
Collection<? extends GrantedAuthority> getAuthorities(); //返回密码
String getPassword(); //返回帐号
String getUsername(); // 账户是否未过期
boolean isAccountNonExpired(); // 账户是否未锁定
boolean isAccountNonLocked(); // 密码是否未过期
boolean isCredentialsNonExpired(); // 账户是否激活
boolean isEnabled();
}
获取基本信息
// 根据用户名查找用户的信息
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

我们只要实现这个扩展,就能够自定义方式获取认证的基本信息

WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter 提供了一种便利的方式去创建 WebSecurityConfigurer的实例,只需要重写 WebSecurityConfigurerAdapter 的方法,即可配置拦截什么URL、设置什么权限等安全控制。

下面是主要会是要到的几个配置:

    /**
* 主要是对身份认证的设置
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
/**
* 复写这个方法来配置 {@link HttpSecurity}.
* 通常,子类不能通过调用 super 来调用此方法,因为它可能会覆盖其配置。 默认配置为:
*
*/
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
} /**
* Override this method to configure {@link WebSecurity}. For example, if you wish to
* ignore certain requests.
* 主要是对某些 web 静态资源的设置
*/
public void configure(WebSecurity web) throws Exception {
}

认证流程

阅读源码了解。

AbstractAuthenticationProcessingFilter.doFilter

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判断是否是需要验证方法(是否是登陆的请求),不是的话直接放过
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
// 登陆的请求开始进行验证
Authentication authResult;
try {
// 开始认证,attemptAuthentication在 UsernamePasswordAuthenticationFilter 中实现
authResult = attemptAuthentication(request, response);
// return null 认证失败
if (authResult == null) {
return;
}
// 篇幅问题,中间很多代码删了
successfulAuthentication(request, response, chain, authResult);
}

UsernamePasswordAuthenticationFilter.attemptAuthentication

// 接收并解析用户登陆信息,为已验证的用户返回一个已填充的身份验证令牌,表示成功的身份验证,
// 如果身份验证过程失败,就抛出一个AuthenticationException
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 方法将 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 对象,用于 AuthenticationManager 的验证
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}

ProviderManager.authenticate

验证 Authentication 对象(里面包含着验证对象)

  1. 如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,覆盖早期支持AuthenticationProviders 所引发的任何可能的AuthenticationException。 成功验证后,将不会尝试后续的AuthenticationProvider。
  2. 如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。

最后它调用的是 Authentication result = provider.authenticate(authentication);

只要我们自定义 AuthenticationProvider 就能完成自定义认证。

动手实现安全框架

使用的依赖

    <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</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>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</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>
</dependencies>

数据表关系

User
@Data
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; @Column(unique = true, nullable = false, length = 50)
private String username; @Column(nullable = false)
private String password; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createDate; @OneToMany(targetEntity = UserRole.class, mappedBy = "userId", fetch = FetchType.EAGER) // mappedBy 只有在双向关联的时候设置,表示关系维护的一端,否则会生成中间表A_B
@org.hibernate.annotations.ForeignKey(name = "none") // 注意这里不能使用 @JoinColumn 不然会生成外键
private Set<UserRole> userRoles;
}
Role
@Entity
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
}
UserRole
@Entity
@Data
public class UserRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; @Column(length = 50, nullable = false)
private Long userId; @ManyToOne(targetEntity = Role.class)
@JoinColumn(name = "roleId", nullable = false, foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
private Role role;
}

流程实现

认证流程:

JWT

我使用的是服务端无状态的token 交换的形式,所以引用的是 jwt,首先实现 jwt:

# jwt 配置
jwt:
# 加密密钥
secret: 61D73234C4F93E03074D74D74D1E39D9 #blog.wuwii.com
# token有效时长
expire: 7 # 7天,单位天
# token 存在 header 中的参数
header: token @ConfigurationProperties(prefix = "jwt")
@Data
public class JwtUtil {
/**
* 密钥
*/
private String secret;
/**
* 有效期限
*/
private int expire;
/**
* 存储 token
*/
private String header; /**
* 生成jwt token
*
* @param username
* @return token
*/
public String generateToken(String username) {
Date nowDate = new Date(); return Jwts.builder()
.setHeaderParam("typ", "JWT")
// 后续获取 subject 是 username
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(DateUtils.addDays(nowDate, expire))
// 这里我采用的是 HS512 算法
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
} /**
* 解析 token,
* 利用 jjwt 提供的parser传入秘钥,
*
* @param token token
* @return 数据声明 Map<String, Object>
*/
private Claims getClaimByToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
return null;
}
} /**
* token是否过期
*
* @return true:过期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
} public String getUsernameFromToken(String token) {
if (StringUtils.isBlank(token)) {
throw new KCException("无效 token", HttpStatus.UNAUTHORIZED.value());
}
Claims claims = getClaimByToken(token);
if (claims == null || isTokenExpired(claims.getExpiration())) {
throw new KCException(header + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
}
return claims.getSubject();
}
}
实现 UserDetails 和 UserDetailsService

实现 UserDetails
public class UserDetailsImpl implements UserDetails {
private User user; public UserDetailsImpl(User user) {
this.user = user;
} /**
* 获取权限信息
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<UserRole> userRoles = user.getUserRoles();
List<GrantedAuthority> auths = new ArrayList<>(userRoles.size());
userRoles.parallelStream().forEach(userRole -> {
// 默认ROLE_ 为前缀,可以更改
auths.add(new SimpleGrantedAuthority("ROLE_" + userRole.getRole().getName()));
});
return auths;
} @Override
public String getPassword() {
return user.getPassword();
} @Override
public String getUsername() {
return user.getUsername();
} // 账户是否未过期
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
} // 账户是否未锁定
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
} // 密码是否未过期
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
} // 账户是否激活
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
实现 UserDetailsService
@Slf4j
@CacheConfig(cacheNames = "users")
public class UserDetailServiceImpl implements UserDetailsService { @Autowired
private UserDao userDao; @Override
@Cacheable
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Username is not valid.");
}
log.debug("The User is {}", user);
return SecurityModelFactory.create(user);
}
}
SecurityModelFactory

转换 UserDetails 的工厂类

public class SecurityModelFactory {
public static UserDetails create(User user) {
return new UserDetailsImpl(user);
}
}
授权认证
登陆过滤器
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private JwtUtil jwtUtil; /**
* 过滤,我目前使用的是默认的,可以自己看源码按需求更改
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// todo 在这里可以按需求进行过滤,根据源码来修改扩展非常方便
super.doFilter(request, response, chain);
} /**
* 如果需要进行登陆认证,会在这里进行预处理
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// todo 在登陆认证的时候,可以做些其他的验证操作,比如验证码
return super.attemptAuthentication(request, response);
} /**
* 登陆成功调用,返回 token
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException {
String token = jwtUtil.generateToken(authResult.getName());
response.setStatus(HttpStatus.OK.value());
response.getWriter().print(token);
}
}
  1. 首先会进入 doFilter 方法中,这里可以自定义定义过滤;
  2. 然后如果是登陆的请求,会进入 attemptAuthentication 组装登陆信息,并且进行登陆认证;
  3. 如果登陆成功,会调用 successfulAuthentication方法。
登陆验证
@Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService; @Autowired
private PasswordEncoder passwordEncoder; /**
* 验证登录信息,若登陆成功,设置 Authentication
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, user.getPassword())) {
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(username, password, authorities);
}
throw new BadCredentialsException("The password is not correct.");
} /**
* 当前 Provider 是否支持对该类型的凭证提供认证服务
*/
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}

我们自己定义的 AuthenticationProvider 主要是实现前面经过过滤器封装的认证对象 UsernamePasswordAuthenticationToken 进行解析认证,

如果认证成功 就给改 UsernamePasswordAuthenticationToken 设置对应的权限,然后返回 Authentication

  1. 获得认证的信息;
  2. 去数据库查询信息,获取密码解密验证认证信息;
  3. 认证成功,设置权限信息,返回 Authentication,失败抛出异常。
JWT 拦截器
/**
* token 校验
* BasicAuthenticationFilter 滤器负责处理任何具有HTTP请求头的请求的请求,
* 以及一个基本的身份验证方案和一个base64编码的用户名:密码令牌。
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService; public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
} /**
* 在此方法中检验客户端请求头中的token,
* 如果存在并合法,就把token中的信息封装到 Authentication 类型的对象中,
* 最后使用 SecurityContextHolder.getContext().setAuthentication(authentication); 改变或删除当前已经验证的 pricipal
*
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = request.getHeader(jwtUtil.getHeader()); //判断是否有token
if (token == null) {
chain.doFilter(request, response);
return;
}
// 通过token 获取账户信息,并且存入到将身份信息存放在安全系统的上下文。
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
chain.doFilter(request, response);
} /**
* 解析token中的信息
*/
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
}
return null;
}
}
  1. 请求进入 doFilterInternal 方法中,对请求是否带token进行判断,
  2. 如果没有token,则直接放行请求;
  3. 如果有 token,则解析它的 post;
配置权限和相关设置

自定义配置 Spring Security 配置类 WebSecurityConfig,进项相关配置,并且将所需要的类注入到系统中。

@Configuration
@EnableWebSecurity // 开启 Security
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
//jsr250Enabled有三种注解,分别是@RolesAllowed,@PermitAll,@DenyAll,功能跟名字一样,
// securedEnabled 开启注解
// prePostEnabled 类似用的最多的是 @PreAuthorize
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public JwtUtil jwtUtil() {
return new JwtUtil();
} /**
* 注入 LoginFilter 时候需要,注入 authenticationManager
*/
@Bean
public LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationManager(authenticationManager());
return loginFilter;
} @Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
return new JwtAuthenticationFilter(authenticationManager());
}
@Bean
public UserDetailsService customService() {
return new UserDetailServiceImpl();
} /**
* 认证 AuthenticationProvider
*/
@Bean
public AuthenticationProvider authenticationProvider() {
return new CustomAuthenticationProvider();
} /**
* BCrypt算法免除存储salt
* BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(5);
} /**
* 主要是对身份验证的设置
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth
// 注入身份的 Bean
.authenticationProvider(authenticationProvider())
.userDetailsService(userDetailsService())
// 默认登陆的加密,自定义登陆的时候无效
.passwordEncoder(passwordEncoder());
// 在内存中设置固定的账户密码以及身份信息
/*auth
.inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER", "ADMIN");*/
} /**
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭 csrf
.csrf().disable()
// 设置 session 状态 STATELESS 无状态
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 需要权限验证
.mvcMatchers("/user/**").authenticated()
.and()
// 登陆页面
.formLogin()
//.loginPage("/login.html")
// 登陆成功跳转页面
.defaultSuccessUrl("/")
//.failureForwardUrl("/login.html")
.permitAll()
.and()
// 登出
//.logout()
// 注销的时候删除会话
//.deleteCookies("JSESSIONID")
// 默认登出请求为 /logout,可以用下面自定义
//.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
// 自定义登出成功的页面,默认为登陆页
//.logoutSuccessUrl("/logout.html")
//.permitAll()
//.and()
// 开启 cookie 保存用户信息
//.rememberMe()
// cookie 有效时间
//.tokenValiditySeconds(60 * 60 * 24 * 7)
// 设置cookie 的私钥,默认为随机生成的key
//.key("remember")
//.and()
//验证登陆的 filter
.addFilter(loginFilter())
//验证token的 filter
.addFilter(jwtAuthenticationFilter());
} /**
* Web层面的配置,一般用来配置无需安全检查的路径
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers(
"**.js",
"**.css",
"/images/**",
"/webjars/**",
"/**/favicon.ico"
);
}
}
权限控制
@RestController
@RequestMapping(value = "/user", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@PreAuthorize("hasRole('USER')")
public class UserController { @Autowired
private UserService userService; @GetMapping
@PreAuthorize("hasRole('admin')")
public ResponseEntity<List<UserVO>> getAllUser() {
List<User> users = userService.findAll();
List<UserVO> userViews = userService.castUserVO(users);
return ResponseEntity.ok(userViews);
}
}

请求上面的getAllUser 方法,需要当前用户同时拥有 ROLE_USERROLE_admin 两个权限,才能通过权限验证。

在 @PreAuthorize 中我们可以利用内建的 SPEL 表达式:比如 ‘hasRole()’ 来决定哪些用户有权访问。需注意的一点是 hasRole 表达式认为每个角色名字前都有一个前缀 ‘ROLE_’。

迭代上个版本

后来,我发现进行用户认证的时候,会将所有的 provider 都尝试一遍,那么外面将登陆的 UsernameAndPasswordTokenJwtTToken 都可以分别进行验证进行了啊,所有我预先定义 UsernamePasswordAuthenticationToken 包装登陆的信息,然后进入登陆的 AuthenticationProvider 进行认证,token 验证形式,使用 PreAuthenticatedAuthenticationToken 的包装,然后进入例外一个 `AuthenticationProvider ` 中认证。

现在我们的流程就更加清晰了。

所以现在我对以前的权限配置以及认证进行了一些更改:

过滤器

在这里,我根据不同请求的类型,进行不同的适配,然后进行加工分装成不同的认证凭证,然后根据凭证的不同,进行不同的认证。

@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private JwtUtil jwtUtil; @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
if (isLoginRequest(httpRequest, httpResponse)) {
Authentication authResult = processLogin(httpRequest, httpResponse);
successfulAuthentication(httpRequest, httpResponse, chain, authResult);
return;
}
String token = obtainToken(httpRequest);
if (StringUtils.isNotBlank(token)) {
processTokenAuthentication(token);
}
} catch (AuthenticationException e) {
unsuccessfulAuthentication(httpRequest, httpResponse, e);
return;
}
chain.doFilter(request, response);
}
/**
* 登陆成功调用,返回 token
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException {
String token = jwtUtil.generateToken(authResult.getName());
response.setStatus(HttpStatus.OK.value());
response.getWriter().print(token);
} private boolean isLoginRequest(HttpServletRequest request, HttpServletResponse response) {
return requiresAuthentication(request, response) && "POST".equalsIgnoreCase(request.getMethod());
} private String obtainToken(HttpServletRequest request) {
return request.getHeader(jwtUtil.getHeader());
} private Authentication processLogin(HttpServletRequest request, HttpServletResponse response) {
String username = obtainUsername(request);
String password = obtainPassword(request);
return tryAuthenticationWithUsernameAndPassword(username, password);
} private void processTokenAuthentication(String token) {
Authentication resultOfAuthentication = tryToAuthenticateWithToken(token);
// 设置上下文用户信息以及权限
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
} private Authentication tryAuthenticationWithUsernameAndPassword(String username, String password) {
Authentication authentication = new UsernamePasswordAuthenticationToken(username, password);
return tryToAuthenticate(authentication);
} private Authentication tryToAuthenticateWithToken(String token) {
PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token, null);
return tryToAuthenticate(requestAuthentication);
} private Authentication tryToAuthenticate(Authentication requestAuth) {
Authentication responseAuth = getAuthenticationManager().authenticate(requestAuth);
if (responseAuth == null || !responseAuth.isAuthenticated()) {
throw new InternalAuthenticationServiceException("Unable to authenticate User for provided credentials");
}
log.debug("User successfully authenticated");
return responseAuth;
}
}

授权认证

根据提供的凭证的类型,进行相关的验证操作

LoginAuthenticationProvider

跟上个版本的 登陆验证中的 CustomAuthenticationProvider 代码一样实现一样。

TokenAuthenticateProvider

根据 token 查找它的 权限 信息,并装在到认证的凭证中。

public class TokenAuthenticateProvider implements AuthenticationProvider {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService; @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String token = authentication.getName();
String username = jwtUtil.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new PreAuthenticatedAuthenticationToken(username, null, userDetails.getAuthorities());
} @Override
public boolean supports(Class<?> authentication) {
return PreAuthenticatedAuthenticationToken.class.equals(authentication);
}
}

配置权限和相关设置

和上个版本没什么变化,只是将类换了一下

@Configuration
@EnableWebSecurity // 开启 Security
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public JwtUtil jwtUtil() {
return new JwtUtil();
} @Bean
public UserDetailsService customService() {
return new UserDetailServiceImpl();
} @Bean("loginAuthenticationProvider")
public AuthenticationProvider loginAuthenticationProvider() {
return new LoginAuthenticationProvider();
} @Bean("tokenAuthenticationProvider")
public AuthenticationProvider tokenAuthenticationProvider() {
return new TokenAuthenticateProvider();
} @Bean
public AuthenticationFilter authenticationFilter() throws Exception {
AuthenticationFilter authenticationFilter = new AuthenticationFilter();
authenticationFilter.setAuthenticationManager(authenticationManager());
return authenticationFilter;
} @Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(5);
} @Bean
@Override
public UserDetailsService userDetailsService() {
return new UserDetailServiceImpl();
}
/**
* 主要是对身份验证的设置
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(loginAuthenticationProvider())
.authenticationProvider(tokenAuthenticationProvider())
.userDetailsService(userDetailsService()); } @Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭 csrf
.csrf().disable()
// 设置 session 状态 STATELESS 无状态
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 需要权限验证
.mvcMatchers("/user/**").authenticated()
.and()
// 登陆页面
.formLogin()
//.loginPage("/login.html")
// 登陆成功跳转页面
.defaultSuccessUrl("/")
.failureForwardUrl("/login.html")
.permitAll()
.and()
.addFilter(authenticationFilter())
;
}
}

后续完善

  1. 修改密码,登出操作 token 的失效机制;
  2. OAuth2 授权服务器的搭建;
  3. 修改权限后,下次请求刷新权限;
  4. ……

附录一:HttpSecurity常用方法

方法 说明
openidLogin() 用于基于 OpenId 的验证
headers() 将安全标头添加到响应
cors() 配置跨域资源共享( CORS )
sessionManagement() 允许配置会话管理
portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509() 配置基于x509的认证
rememberMe 允许配置“记住我”的验证
authorizeRequests() 允许基于使用HttpServletRequest限制访问
requestCache() 允许配置请求缓存
exceptionHandling() 允许配置错误处理
securityContext() HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi() HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic() 配置 Http Basic 验证
addFilterAt() 在指定的Filter类的位置添加过滤器

学习Spring Boot:(二十八)Spring Security 权限认证的更多相关文章

  1. Spring Boot(二十):使用spring-boot-admin对spring-boot服务进行监控

    Spring Boot(二十):使用spring-boot-admin对spring-boot服务进行监控 Spring Boot Actuator提供了对单个Spring Boot的监控,信息包含: ...

  2. spring boot / cloud (十八) 使用docker快速搭建本地环境

    spring boot / cloud (十八) 使用docker快速搭建本地环境 在平时的开发中工作中,环境的搭建其实一直都是一个很麻烦的事情 特别是现在,系统越来越复杂,所需要连接的一些中间件也越 ...

  3. spring boot(二十)使用spring-boot-admin对服务进行监控

    上一篇文章<springboot(十九):使用Spring Boot Actuator监控应用>介绍了Spring Boot Actuator的使用,Spring Boot Actuato ...

  4. (转)Spring Boot(二十):使用 spring-boot-admin 对 Spring Boot 服务进行监控

    http://www.ityouknow.com/springboot/2018/02/11/spring-boot-admin.html 上一篇文章<Spring Boot(十九):使用 Sp ...

  5. Spring Boot2 系列教程(二十八)Spring Boot 整合 Session 共享

    这篇文章是松哥的原创,但是在第一次发布的时候,忘了标记原创,结果被好多号转发,导致我后来整理的时候自己没法标记原创了.写了几百篇原创技术干货了,有一两篇忘记标记原创进而造成的一点点小小损失也能接受,不 ...

  6. (转)Spring Boot 2 (十):Spring Boot 中的响应式编程和 WebFlux 入门

    http://www.ityouknow.com/springboot/2019/02/12/spring-boot-webflux.html Spring 5.0 中发布了重量级组件 Webflux ...

  7. Spring Boot 2 (十):Spring Boot 中的响应式编程和 WebFlux 入门

    Spring 5.0 中发布了重量级组件 Webflux,拉起了响应式编程的规模使用序幕. WebFlux 使用的场景是异步非阻塞的,使用 Webflux 作为系统解决方案,在大多数场景下可以提高系统 ...

  8. Dynamic CRM 2013学习笔记(二十八)用JS动态设置字段的change事件、必填、禁用以及可见

    我们知道通过界面设置字段的change事件,是否是必填,是否可见非常容易.但有时我们需要动态地根据某些条件来设置,这时有需要通过js来动态地控制了. 下面分别介绍如何用js来动态设置.   一.动态设 ...

  9. 【Java学习笔记之二十八】深入了解Java8新特性

    前言: Java8 已经发布很久了,很多报道表明java8 是一次重大的版本升级.在Java Code Geeks上已经有很多介绍Java 8新特性的文章,例如Playing with Java 8 ...

  10. Linux学习之CentOS(二十八)--RAID原理基础及Linux下软件RAID配置

    一.RAID的原理基础 在 讲解RAID的原理基础之前,我们首先来了解一下传统磁盘的劣势.我们知道一台PC机种都会包含CPU.内存.主板.硬盘.网卡等硬件,影响计算机性能的 组建包括:CPU.主板总线 ...

随机推荐

  1. mysql利用binlog进行数据恢复

    目录 mysql利用binlog进行数据恢复 binlog基本配置和格式 binlog基本配置 查看binlog状态 binlog的三种格式 转换成sql mysql自带的mysqlbinlog 利用 ...

  2. Luogu P3388 【模板】割点(割顶)

    一道求割点的板子题.还是采用经典的Tarjan算法. 首先大致和Tarjan求强连通分量相似,都是用\(dfn_x\)表示访问到\(x\)的时间(时间戳),\(low_x\)表示通过\(x\)回边能走 ...

  3. [Spark][Python]Mapping Single Rows to Multiple Pairs

    Mapping Single Rows to Multiple Pairs目的: 把如下的这种数据, Input Data 00001 sku010:sku933:sku02200002 sku912 ...

  4. vue-cli 3.0 路由懒加载

    当打包构建应用时,Javascript 包会变得非常大,影响页面加载.如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了. 1. 安装 synta ...

  5. CSS 分类 (Classification) 实例

    CSS 分类 (Classification) 实例CSS 分类属性 (Classification)CSS 分类属性允许你控制如何显示元素,设置图像显示于另一元素中的何处,相对于其正常位置来定位元素 ...

  6. Azure Load Balancer : 支持 IPv6

    越来越多的网站开始支持 IPv6,即使是哪些只提供 api 服务的站点也需要支持 IPv6,比如苹果应用商店中的 app 早就强制要求服务器端支持 IPv6 了.笔者在前文<Azure Load ...

  7. CentOS7中安装redis5.0

    1. 环境介绍 CentOS7 (未安装Development Tools) 2. 下载Redis5.0-rc3 wget -O redis-5.0-rc3.tar.gz https://github ...

  8. getUserMedia API及HTML5 调用摄像头和麦克风

    getUserMedia API简介 HTML5的getUserMedia API为用户提供访问硬件设备媒体(摄像头.视频.音频.地理位置等)的接口,基于该接口,开发者可以在不依赖任何浏览器插件的条件 ...

  9. C_数据结构_递归A函数调用B函数

    # include <stdio.h> int g(int); int f(int); int f(int n) { ) printf("haha\n"); else ...

  10. 使用highcharts绘制美观的燃尽图

    使用highcharts绘制美观的燃尽图 助教在博客中介绍了两种绘制燃尽图的方法,但是我们组在使用时发现有些任务不适合写进issue,而且网站生成的燃尽图不是很美观,因此我们打算使用其他方法自己绘制燃 ...