18.1 The application senario
- Add a new workout record for a user
- Find all the workouts for a user
- Delete a workout
18.2 Configuring Keycloak as an authorization server
docker 安装 Keycloak
docker network create keycloak-network
docker run --name mysql -d -p 3306:3306 --net keycloak-network -e MYSQL_DATABASE=keycloak -e MYSQL_USER=keycloak -e MYSQL_PASSWORD=keycloak -e MYSQL_ROOT_PASSWORD=keycloak mysql:5.7
docker run --name keycloak -d -p 8080:8080 --net keycloak-network -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin -e DB_USER=keycloak -e DB_PASSWORD=keycloak -e JDBC_PARAMS='useSSL=false' jboss/keycloak
访问:101.132.251.198:8080
进入控制台页面会报错:
解决方案:
注意修改
docker run -d -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD='admin1024' -e KEYCLOAK_BIND_ADDRESS=0.0.0.0 -e KEYCLOAK_ENABLE_TLS=false quay.io/keycloak/keycloak:15.0.2'
docker exec -it {contaierID} bash
cd /opt/jboss/keycloak/bin
./kcadm.sh config credentials --server http://localhost:8080/auth --realm master --user admin
./kcadm.sh update realms/master -s sslRequired=NONE
这个配置提供了token endpoint,authorization endpoint还有grand types列表
进一步配置authorization server:
- 为系统注册一个client
- 定义一个client scope
- 添加users
- 定义user roles以及自定义的访问tokens
18.2.1 Registering a client for our system
18.2.2 Specifying client scopes
18.2.3 Adding users and obtaining access tokens
调用授权服务的/token端点:
发送请求之后:
对这个token解码之后发现:roles以及username都没有,所以接下来应该分配roles给users以及自定义jwt去包含所有的数据
18.2.4 Defining the user roles
创建roles
将roles分配给users
将mary设置为fitnessadmin其他的为fitnessuser
我们还需要添加三个细节到tokens中:
- Roles
- Username
- Audience claim
18.3 Implementing the resource server
依赖:
<dependencies>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
导入sql文件
配置文件:
定义entity
@Entity
@Data
public class Workout {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String user;
private LocalDateTime start;
private LocalDateTime end;
private int difficulty;
}
定义repository
public interface WorkoutRepository extends JpaRepository<Workout, Integer> {
@Query("select w from Workout w where w.user = ?#{authentication.name}")
List<Workout> findAllByUser();
}
定义service
@Service
public class WorkoutService {
@Autowired
private WorkoutRepository workoutRepository;
@PreAuthorize("#workout.user == authentication.name")
public void saveWorkout(Workout workout){
workoutRepository.save(workout);
}
public List<Workout> findWorkouts(){
return workoutRepository.findAllByUser();
}
public void deleteWorkout(Integer id){
workoutRepository.deleteById(id);
}
}
定义controller
@RestController
@RequestMapping("/workout")
public class WorkoutController {
@Autowired
private WorkoutService workoutService;
@PostMapping("/")
public void add(@RequestBody Workout workout){
workoutService.saveWorkout(workout);
}
@GetMapping("/")
public List<Workout> findAll(){
return workoutService.findWorkouts();
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Integer id){
workoutService.deleteWorkout(id);
}
}
添加两个自定义配置:
定义配置类:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${claim.aud}")
private String claimAud;
@Value("${jwkSetUri}")
private String urlJwk;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore());
resources.resourceId(claimAud);
}
@Bean
public TokenStore tokenStore() {
return new JwkTokenStore(urlJwk);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers(HttpMethod.DELETE, "/**")
.hasAnyAuthority("fitnessadmin")
.anyRequest().authenticated();
}
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension(){
return new SecurityEvaluationContextExtension();
}
}
18.4 Testing the application
18.4.1 Proving an authenticated user can only add a record for themself
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsQk9DX2VaSTdiTklzVVRadEszTlpaMDUybnpOQUFMb25XMmZ4aFpTb0JZIn0.eyJleHAiOjE2NTM3OTgwNzMsImlhdCI6MTY1Mzc5ODAxMywianRpIjoiYzQ1ZDkwZjctNmE4Mi00NmRjLWJjZjUtMzZiYjM4YTMyMjQ1IiwiaXNzIjoiaHR0cDovLzEwMS4xMzIuMjUxLjE5ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImZpdG5lc3NhcHAiLCJzdWIiOiJlOGNlNjkwMC0yMTY5LTRlOTctOGJlOC1hZTI1NGNkNjliNDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmaXRuZXNzYXBwIiwic2Vzc2lvbl9zdGF0ZSI6IjQxMWRiN2M0LTUzMDMtNDNjZS04NmY1LWE1M2NhNWNkMDZiNCIsImFjciI6IjEiLCJzY29wZSI6ImZpdG5lc3NhcHAiLCJzaWQiOiI0MTFkYjdjNC01MzAzLTQzY2UtODZmNS1hNTNjYTVjZDA2YjQiLCJ1c2VyX25hbWUiOiJiaWxsIiwiYXV0aG9yaXRpZXMiOlsiZml0bmVzc3VzZXIiXX0.HEG7B7ICjeSjIixTU4jgx6r4V6jfLFU7l2cXEQIiVlTB1Q8j3ForbIY7QbkBb6w-yp1GbirBUTsfF7RDgK1vHWEe8sgVHIsjWPXcwLIh9DHG63b3h9sbPmPqGU53ZIuxZSwOzhrHn_y9reS0weskoU5fvhWk3YP84Te4vWCxtPVAJ650NG49ZzsND5pMWKQQmLfjm-apB9h9wZW8Zy9eH9aHOjwQvCqvdTaDM6dZu2AHnYwuCYZJe8Pslgyle7rIHJJzgXqguFeiDXBNDCNirTLtuiosaMmZuihhtN_DgXc6UcWE-RLlDAeteT5eoZq7r2eMDBPwu4GT8RN0tBaGfw",
"expires_in": 60,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkNjVhYzIwOC1lMTVhLTRhMGItOWNmMy1hNmExZTA1MDBiOGUifQ.eyJleHAiOjE2NTM3OTk4MTMsImlhdCI6MTY1Mzc5ODAxMywianRpIjoiYjBiYTM1ZTItZDc3MC00YTRjLWI2MGQtNTgwNjdkZGRjYmY1IiwiaXNzIjoiaHR0cDovLzEwMS4xMzIuMjUxLjE5ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6Imh0dHA6Ly8xMDEuMTMyLjI1MS4xOTg6ODA4MC9hdXRoL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiJlOGNlNjkwMC0yMTY5LTRlOTctOGJlOC1hZTI1NGNkNjliNDAiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiZml0bmVzc2FwcCIsInNlc3Npb25fc3RhdGUiOiI0MTFkYjdjNC01MzAzLTQzY2UtODZmNS1hNTNjYTVjZDA2YjQiLCJzY29wZSI6ImZpdG5lc3NhcHAiLCJzaWQiOiI0MTFkYjdjNC01MzAzLTQzY2UtODZmNS1hNTNjYTVjZDA2YjQifQ.JMildS3qX1hV2P7jGVr96Zkj3mDL5AcVYAs7PKEEn9M",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "411db7c4-5303-43ce-86f5-a53ca5cd06b4",
"scope": "fitnessapp"
}
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJsQk9DX2VaSTdiTklzVVRadEszTlpaMDUybnpOQUFMb25XMmZ4aFpTb0JZIn0.eyJleHAiOjE2NTM4MDA5NDQsImlhdCI6MTY1MzgwMDg4NCwianRpIjoiM2U5MDQyNGMtMDY4MC00Njg0LTgxNmMtMDFmMjQwMGY0ZTMxIiwiaXNzIjoiaHR0cDovLzEwMS4xMzIuMjUxLjE5ODo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImZpdG5lc3NhcHAiLCJzdWIiOiJlOGNlNjkwMC0yMTY5LTRlOTctOGJlOC1hZTI1NGNkNjliNDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmaXRuZXNzYXBwIiwic2Vzc2lvbl9zdGF0ZSI6IjI3YjY4MjAzLWIwNDYtNDc5ZS05YmZhLWE4NDllZTAxN2Y4ZCIsImFjciI6IjEiLCJzY29wZSI6ImZpdG5lc3NhcHAiLCJzaWQiOiIyN2I2ODIwMy1iMDQ2LTQ3OWUtOWJmYS1hODQ5ZWUwMTdmOGQiLCJ1c2VyX25hbWUiOiJiaWxsIiwiYXV0aG9yaXRpZXMiOlsiZml0bmVzc3VzZXIiXX0.hesAxNDtMcSsbaeCsF430TMRJ8CAlQnjftnG1WA1tt6_RfVVqXr0v8j1y3vPX9ytWYhxmZ8IVphYp2VbpLog1G0ORMroAFPiqqAScX-GmVdPBPF6dkPRLAGUnRpjxwYIxtia-FQIFbSZYP8o-Bg0ZysADlvgzVa6xKMT246NbOQJbAtcE4N8z-jgXX5JtrTqTltuWzXoGDY_P-g-mrPCpsq0bgnSMmzUflK5epACrTpaEvT4hZ_ylODCO5Iq2Ri3fZFtQkzllpDcxgSweuJfSqrRKYr-Ha3xZyiUUplMyNwqamH0CHpREX0Hr2DsUQDAr06j7J_em0ATZqubIQ-RCA