为了实现在登录界面登录后,把token写入cookie的效果,需要配置successHandler,继承SavedRequestAwareAuthenticationSuccessHandler在页面登录成功后生成token
上一话生成的是jwt,但在successHandler中生成的token不是jwtToken(不知道该怎么生成才好),而且生成的token无法鉴权成功,猜测是无法读取到身份信息,所以改用RedisTokenStore。
服务端
绘制一个登录界面
1、绘制界面使用的是bootstrap,也用到了jquery,密码使用md5进行初步加密,所以导入以下依赖

2、使用thymeleaf,导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring:
thymeleaf:
cache: false #关闭缓存
3、绘制登录界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<link href="assets/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/css/signin.css" rel="stylesheet">
<script src="assets/js/jquery-3.5.1.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/md5.js"></script>
</head>
<body class="text-center">
<!--<form class="form-signin" action="/form-login" method="post">-->
<div class="form-signin">
<img class="mb-4" src="assets/img/jzo.png" alt="" width="210" height="72">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label class="sr-only">Username</label>
<input type="text" name="username" class="form-control" placeholder="Username"
required="" autofocus="">
<label class="sr-only">Password</label>
<input type="password" name="password" class="form-control" placeholder="Password"
required="">
<input type="hidden" name="scope" value="all"
required="" autofocus="">
<input type="hidden" name="grant_type" value="password"
required="" autofocus="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"/> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" onclick="login()">Sign in</button>
<p class="mt-5 mb-3 text-muted">© jenson</p>
</div>
<script type="text/javascript">
function login() {
$.ajax({
type: "POST",
url: "/form-login",
data: {
username: $("input[name='username']").val(),
password: hex_md5($("input[name='password']").val()),
scope: $("input[name='scope']").val(),
grant_type: $("input[name='grant_type']").val()
},
dataType: "json",
headers: {"Authorization": "Basic anplcm8tY2xpZW50Omp6ZXJvLXNlY3JldA=="},
async: false
// success: function (data) {
// }
});
}
</script>
</body>
</html>
为了调用接口时带上headers,采用ajax。
4、编写Controller
@Controller
public class LoginController {
@GetMapping("/login")
public String login(@RequestParam(value = "error", required = false) String error, Model model){
if (error != null) {
return "loginFailure";
}
return "login";
}
}
5、在WebSecurity中配置登录页,完整代码如下
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private BaseUserDetailsService baseUserDetailsService;
@Autowired
private UserAuthenticationSuccessHandler userAuthenticationSuccessHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 允许匿名访问所有接口 主要是 oauth 接口
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.authorizeRequests()
// .antMatchers("/**").permitAll();
http
// 配置登陆页/login并允许访问
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/form-login")
.defaultSuccessUrl("/index")
.successHandler(userAuthenticationSuccessHandler)
.failureUrl("/login?error=error")
.permitAll()
// 登出页
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 其余所有请求全部需要鉴权认证
.and().authorizeRequests().antMatchers("/**").permitAll()
.and().csrf().disable();
}
/**
* 用户验证
* @param auth
*/
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(baseUserDetailsService).passwordEncoder(passwordEncoder());
}
}
6、登录页面如下

RedisToken 配置及登录成功后successHandler实现
1、redis数据库依赖和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
redis:
host: redis.jzero.org
port: 6379
database: 1
2、创建 redisTokenStore 的bean
@Configuration
public class RedisTokenConfig {
@Bean
public TokenStore redisTokenStore(RedisConnectionFactory redisConnectionFactory) {
return new RedisTokenStore(redisConnectionFactory);
}
}
3、 实现UserDetailsService,从数据库中获取密码,此处使用的是jpa来实现数据库操作
@Slf4j
@Component
public class BaseUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username is:" + username);
// 用户角色也应在数据库中获取
String role;
String password;
if ("admin".equals(username)) {
// 特殊账号
role = "ROLE_ADMIN";
// 线上环境应该通过用户名查询数据库获取加密后的密码
password = passwordEncoder.encode(DigestUtils.md5DigestAsHex("123456".getBytes()));
System.out.println("password:" + password);
} else {
// 从数据库中获取密码
role = "ROLE_USER";
org.jzero.oauth.domain.entity.User user = userRepository.findByUsername(username);
password = user.getPassword();
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(role));
return new User(username, password, authorities);
}
}
4、在 AuthorizationServerConfigurer 中使用 redisTokenStore ,完整代码如下
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public UserDetailsService baseUserDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore redisTokenStore;
@Autowired
private AuthorizationServerTokenServices redisTokenService;
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 集成websecurity认证
endpoints.authenticationManager(authenticationManager);
endpoints.tokenServices(redisTokenService);
// 注册redis令牌仓库
endpoints.tokenStore(redisTokenStore);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("jzero-client")
.secret(passwordEncoder.encode("jzero-secret"))
// .authorizedGrantTypes("refresh_token", "authorization_code", "password")
.authorizedGrantTypes("password")
// .accessTokenValiditySeconds(3600)
.scopes("all");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
security.tokenKeyAccess("permitAll()");
}
}
5、为了在successHandler中生成token,需要自己实现TokenService
@Configuration
public class TokenServicesConfig {
@Autowired
private TokenStore redisTokenStore;
@Bean
public AuthorizationServerTokenServices redisTokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setTokenStore(redisTokenStore);
service.setAccessTokenValiditySeconds(600);
service.setRefreshTokenValiditySeconds(7200);
return service;
}
}
6、进入正题,继承SavedRequestAwareAuthenticationSuccessHandler,编写successHandler生成token
@Slf4j
@Component
public class UserAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices redisTokenService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String header = request.getHeader("Authorization");
String username = authentication.getName();
if (header == null || !header.toLowerCase().startsWith("basic ")) {
throw new UnapprovedClientAuthenticationException("请求头中无client信息");
}
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String clientId = tokens[0];
String clientSecret = tokens[1];
//拿到了clientDetails
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
//校验
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("client-id对应信息不存在:" + clientId);
} else if (!new BCryptPasswordEncoder().matches(clientSecret, clientDetails.getClientSecret())) {
throw new UnapprovedClientAuthenticationException("client-secret对应信息不匹配:" + clientSecret);
}
TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "password");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken token = redisTokenService.createAccessToken(oAuth2Authentication);
// jwtTokenStore.storeAccessToken(token, oAuth2Authentication);
// 将session 写入cookie
Cookie cookie = new Cookie("access_token", token.getValue());
// cookie.setMaxAge(60); //存活一分钟
cookie.setMaxAge(60 * 60); //存活一小时
// cookie.setMaxAge(24*60*60); //存活一天
// cookie.setMaxAge(365*24*60*60); //存活一年
response.addCookie(cookie);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(token));
System.out.println("ok");
}
/**
* Decodes the header into a username and password.
*
* @throws BadCredentialsException if the Basic header is not present or is not valid
* Base64
*/
private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException(
"Failed to decode basic authentication token");
}
// String token = new String(decoded, getCredentialsCharset(request));
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[]{token.substring(0, delim), token.substring(delim + 1)};
}
}
参考
参考文档:https://segmentfault.com/a/1190000016583573?utm_source=tag-newest
网友评论