Peter 老师继续聊聊项目5
一、总体流程与思路:
总体流程其实就是如下,这里很明确的,有资源服务器,认证服务器等。
-用户打开客户端后,客户端要求用户给予授权。
-用户同意给予客户端授权。
-客户端使用授权得到的code,向认证服务器申请token令牌。
-认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
-客户端请求资源时携带token令牌,向资源服务器申请获取资源。
-资源服务器确认令牌无误,同意向客户端开放资源。
基本的思路就是:
实现OAuth2.0单点登陆需要准备3个Springboot服务
1)资源服务
2)授权服务
3)用户访问服务
二、代码详解:
项目5 其实不是要让学员实现什么,学员需要的代码,几乎都给出了,项目5 的目的其实是让学员把代码组成项目工程,一个完整的项目工程文件。其中同学问了一个很关键的问题:
configure目录是干啥的?其实configure目录是登陆的核心服务在那里。
- 资源服务:
/**
* 资源服务器
**/
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**/*.js","/**/*.html","/**/*.css", "/oauth/**","/**/*.jpg","/**/*.png","/**/*.ttf","/**/*.woff","/**/*.woff2").permitAll()
//其他的请求都必须要有权限认证
.anyRequest()
.authenticated()
.and()
.formLogin()//允许用户进行基于表单的认证
.loginPage("/login.html")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHandler)
.and()
.headers().frameOptions().disable()
.and()
// 暂时禁用CSRF,否则无法提交登录表单
.csrf().disable();
}
public MyAuthenticationSuccessHandler loginSuccessHandler(){
return new MyAuthenticationSuccessHandler();
}
}
由于登录分为成功和失败两种情况。
登录认证失败,则重新登录
登录认证成功,此处是密码授权方式,我们将成功信息包括tokenId返回给调用方,同时保存用户相关信息,包括用户名、token、登录次数等。
可以定义失败处理器:
登录成功后的处理:
@Slf4j
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Autowired
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
String username = request.getParameter("username");
String password = request.getParameter("password");
String clientId = request.getParameter("clientId");
String clientSecret = request.getParameter("clientSecret");
log.info("userName:"+username);
log.info("password:"+password);
log.info("clientId:"+clientId);
log.info("clientSecret"+clientSecret);
//获取 ClientDetails
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null){
throw new UnapprovedClientAuthenticationException("clientId 不存在"+clientId);
//判断 方言 是否一致
}else if (!StringUtils.equals(clientDetails.getClientSecret(),clientSecret)){
throw new UnapprovedClientAuthenticationException("clientSecret 不匹配"+clientId);
}
//密码授权 模式, 组建 authentication
TokenRequest tokenRequest = new TokenRequest(new HashMap<String,String>(),clientId,clientDetails.getScope(),"password");
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request,authentication);
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
log.info("token:"+JSONUtil.toJSON(token));
//根据用户名和token先查询是否已经存在
Integer count = userService.countLogin(username,token.getValue());
if(null != count && count > 0){
log.info("用户:"+username+"-"+" token:"+token.getValue()+"已经存在");
}else{
//根据用户名称和tokenId保存 登录信息
userService.saveLogin(username,token.getValue());
}
//登录次数 +1
userService.addLogin(username);
//将 authention 信息打包成json格式返回
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
response.getWriter().write(JSONUtil.toJSON(token));
response.sendRedirect("/main.html?tokenId="+token.getValue());
}
}
然后是登陆认证服务。先确定授权,即某某人,某某帐号可以登陆,如某某学员是极客营学员,可以访问极客营资源,再判断他(她)只能访问Java L2 课程资源,他(她)不能访问架构师课程资源。大致就是这个意思。
如果登录成功,我们需要配置认证服务,实现信息存储
认证服务
/**
* 认证服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
DataSource dataSource;
// 声明TokenStore实现 数据库存储
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
// 声明 ClientDetails实现 数据库存储
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 配置token获取和验证时的策略 (Spring Security安全表达式),可以表单提交
security.tokenKeyAccess( "permitAll()").checkTokenAccess("isAuthenticated()").allowFormAuthenticationForClients();
}
/**
* @param clients
* @throws Exception
*
* 客户端模式token请求:
*
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//客户端信息存储数据库
clients.withClientDetails(clientDetails());
}
}
之后我们需要配置资源服务,实现资源权限分配机制
/**
* 资源服务器
**/
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**/*.js","/**/*.html","/**/*.css", "/oauth/**","/**/*.jpg","/**/*.png","/**/*.ttf","/**/*.woff","/**/*.woff2").permitAll()
//其他的请求都必须要有权限认证
.anyRequest()
.authenticated()
.and()
.formLogin()//允许用户进行基于表单的认证
.loginPage("/login.html")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailHandler)
.and()
.headers().frameOptions().disable()
.and()
// 暂时禁用CSRF,否则无法提交登录表单
.csrf().disable();
}
public MyAuthenticationSuccessHandler loginSuccessHandler(){
return new MyAuthenticationSuccessHandler();
}
}
此时,我们还没有加前端代码,不过我们这里可以先配置前端权限
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
/**
* 配置.忽略的静态文件,不加的话,登录之前页面的css,js不能正常使用,得登录之后才能正常.
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略URL
web.ignoring().antMatchers("/resources/**");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
/**
* 重写authenticationManagerBean方法,防止注入失败
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**/*.js","/**/*.html","/**/*.css", "/oauth/**", "/**/*.jpg","/**/*.png","/**/*.ttf","/**/*.woff","/**/*.woff2").permitAll()
//其他的请求都必须要有权限认证
.anyRequest().authenticated()
.and()
// 暂时禁用CSRF,否则无法提交登录表单
.csrf().disable();
http.formLogin() //允许用户进行基于表单的认证
.loginPage("/login.html")
.loginProcessingUrl("/login")
.successHandler(myAuthenticationSuccessHandler);
http.headers().frameOptions().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
用户访问服务:这里可以理解为用户访问的跳转,以及链接配置。
用户访问的跳转,
//*******************************************资源接口
// 新增/修改资源信息
@PostMapping("/resource")
@ResponseStatus(value = HttpStatus.CREATED)
public Integer saveResource(@RequestBody ResourceForm resourceForm) {
Resource resource = copy(resourceForm,Resource.class);
return resourceService.save(resource);
}
@GetMapping("/resource/{id}")
@ResponseStatus(value = HttpStatus.OK)
public ResourceVo findResourceById(@PathVariable("id") @Min(value=1,message="id不能小于1")Integer id) {
Resource resource = resourceService.findById(id);
if(null == resource){
throw new BusinessException("资源信息不存在");
}
log.info("resourceName:"+resource.getName());
log.info("url:"+resource.getUrl());
log.info("method:"+resource.getMethod());
ResourceVo vo = copy(resource,ResourceVo.class);
return vo;
}
@GetMapping("/resource")
@ResponseStatus(value = HttpStatus.OK)
public List<ResourceVo> findAllResources() {
List<Resource> resources = resourceService.findAll();
List<ResourceVo> vos = copyList(resources,ResourceVo.class);
return vos;
}
@DeleteMapping("/resource/{id}")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public Boolean deleteResource(@PathVariable("id") @Min(value=1,message="id不能小于1") Integer id) {
resourceService.delete(id);
return true;
}
用户接口和角色接口的操作可自行添加 LoginController中实现了登录获取资源的接口
/**
* token验证拦截器
*/
@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {
public static final String TOKEN_NAME = "access_token";
private UserService userService;
public AuthenticationInterceptor(UserService userService) {
this.userService = userService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if ("/".equals(request.getRequestURI())) {
return true;
}
return userAuth(request);
}
/**
* 实际用户token鉴权
*
* @param request
*/
private boolean userAuth(HttpServletRequest request) {
String tokenId = getTokenIdFromParamters(request);
/*if (StringUtils.isEmpty(tokenId)) {
throw new BusinessException("您尚未登录,请登录系统!");
}*/
/**
* 这里手动实现权限验证,用户-角色-资源
*/
//获取访问的url
String url = request.getRequestURI();
String method = request.getMethod().toUpperCase();
log.info("url:"+url);
log.info("method:"+method);
return userService.checkLoginUser(url,tokenId,method);
}
/**
* 从参数中获取tokenid
*
* @param request
* @return
*/
private String getTokenIdFromParamters(HttpServletRequest request) {
String tokenId = request.getParameter(TOKEN_NAME);
return tokenId;
}
/**
* 从header获取tokenid
*
* @param request
* @return
*/
private String getTokenIdFromHeader(HttpServletRequest request) {
String tokenId = request.getHeader(TOKEN_NAME);
return tokenId;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
package com.rb.login.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.initialSize}")
private String initialSize;
@Value("${spring.datasource.minIdle}")
private String minIdle;
@Value("${spring.datasource.maxActive}")
private String maxActive;
@Value("${spring.datasource.maxWait}")
private String maxWait;
@Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
private String timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.minEvictableIdleTimeMillis}")
private String minEvictableIdleTimeMillis;
@Value("${spring.datasource.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.testWhileIdle}")
private String testWhileIdle;
@Value("${spring.datasource.testOnBorrow}")
private String testOnBorrow;
@Value("${spring.datasource.testOnReturn}")
private String testOnReturn;
@Value("${spring.datasource.poolPreparedStatements}")
private String poolPreparedStatements;
@Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
private String maxPoolPreparedStatementPerConnectionSize;
@Bean
@Primary
public DataSource dataSource(){
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
//configuration
if(initialSize != null) {
datasource.setInitialSize(Integer.parseInt(initialSize));
}
if(minIdle != null) {
datasource.setMinIdle(Integer.parseInt(minIdle));
}
if(maxActive != null) {
datasource.setMaxActive(Integer.parseInt(maxActive));
}
if(maxWait != null) {
datasource.setMaxWait(Integer.parseInt(maxWait));
}
if(timeBetweenEvictionRunsMillis != null) {
datasource.setTimeBetweenEvictionRunsMillis(Integer.parseInt(timeBetweenEvictionRunsMillis));
}
if(minEvictableIdleTimeMillis != null) {
datasource.setMinEvictableIdleTimeMillis(Integer.parseInt(minEvictableIdleTimeMillis));
}
if(validationQuery!=null) {
datasource.setValidationQuery(validationQuery);
}
if(testWhileIdle != null) {
datasource.setTestWhileIdle(Boolean.valueOf(testWhileIdle));
}
if(testOnBorrow != null) {
datasource.setTestOnBorrow(Boolean.valueOf(testOnBorrow));
}
if(testOnReturn != null) {
datasource.setTestOnReturn(Boolean.valueOf(testOnReturn));
}
if(poolPreparedStatements != null) {
datasource.setPoolPreparedStatements(Boolean.valueOf(poolPreparedStatements));
}
if(maxPoolPreparedStatementPerConnectionSize != null) {
datasource.setMaxPoolPreparedStatementPerConnectionSize(Integer.parseInt(maxPoolPreparedStatementPerConnectionSize));
}
return datasource;
}
}